2 * Copyright (C) 2015 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
16 package com.android.systemui.qs.external;
18 import android.app.ActivityManager;
19 import android.content.ComponentName;
20 import android.content.Intent;
21 import android.content.pm.PackageManager;
22 import android.content.pm.ResolveInfo;
23 import android.content.pm.ServiceInfo;
24 import android.graphics.drawable.Drawable;
25 import android.net.Uri;
26 import android.os.Binder;
27 import android.os.IBinder;
28 import android.os.RemoteException;
29 import android.provider.Settings;
30 import android.service.quicksettings.IQSTileService;
31 import android.service.quicksettings.Tile;
32 import android.service.quicksettings.TileService;
33 import android.text.SpannableStringBuilder;
34 import android.text.style.ForegroundColorSpan;
35 import android.util.Log;
36 import android.view.IWindowManager;
37 import android.view.WindowManager;
38 import android.view.WindowManagerGlobal;
39 import com.android.internal.logging.MetricsLogger;
40 import com.android.internal.logging.MetricsProto.MetricsEvent;
41 import com.android.systemui.R;
42 import com.android.systemui.qs.QSTile;
43 import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
44 import com.android.systemui.statusbar.phone.QSTileHost;
45 import libcore.util.Objects;
47 public class CustomTile extends QSTile<QSTile.State> implements TileChangeListener {
48 public static final String PREFIX = "custom(";
50 private static final boolean DEBUG = false;
52 // We don't want to thrash binding and unbinding if the user opens and closes the panel a lot.
53 // So instead we have a period of waiting.
54 private static final long UNBIND_DELAY = 30000;
56 private final ComponentName mComponent;
57 private final Tile mTile;
58 private final IWindowManager mWindowManager;
59 private final IBinder mToken = new Binder();
60 private final IQSTileService mService;
61 private final TileServiceManager mServiceManager;
62 private final int mUser;
63 private android.graphics.drawable.Icon mDefaultIcon;
65 private boolean mListening;
66 private boolean mBound;
67 private boolean mIsTokenGranted;
68 private boolean mIsShowingDialog;
70 private CustomTile(QSTileHost host, String action) {
72 mWindowManager = WindowManagerGlobal.getWindowManagerService();
73 mComponent = ComponentName.unflattenFromString(action);
74 mTile = new Tile(mComponent);
76 mServiceManager = host.getTileServices().getTileWrapper(this);
77 mService = mServiceManager.getTileService();
78 mServiceManager.setTileChangeListener(this);
79 mUser = ActivityManager.getCurrentUser();
82 private void setTileIcon() {
84 PackageManager pm = mContext.getPackageManager();
85 ServiceInfo info = pm.getServiceInfo(mComponent,
86 PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE);
87 // Update the icon if its not set or is the default icon.
88 boolean updateIcon = mTile.getIcon() == null
89 || iconEquals(mTile.getIcon(), mDefaultIcon);
90 mDefaultIcon = info.icon != 0 ? android.graphics.drawable.Icon
91 .createWithResource(mComponent.getPackageName(), info.icon) : null;
93 mTile.setIcon(mDefaultIcon);
95 // Update the label if there is no label.
96 if (mTile.getLabel() == null) {
97 mTile.setLabel(info.loadLabel(pm));
99 } catch (Exception e) {
105 * Compare two icons, only works for resources.
107 private boolean iconEquals(android.graphics.drawable.Icon icon1,
108 android.graphics.drawable.Icon icon2) {
109 if (icon1 == icon2) {
112 if (icon1 == null || icon2 == null) {
115 if (icon1.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE
116 || icon2.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE) {
119 if (icon1.getResId() != icon2.getResId()) {
122 if (!Objects.equal(icon1.getResPackage(), icon2.getResPackage())) {
129 public void onTileChanged(ComponentName tile) {
134 public boolean isAvailable() {
135 return mDefaultIcon != null;
138 public int getUser() {
142 public ComponentName getComponent() {
146 public Tile getQsTile() {
150 public void updateState(Tile tile) {
151 mTile.setIcon(tile.getIcon());
152 mTile.setLabel(tile.getLabel());
153 mTile.setContentDescription(tile.getContentDescription());
154 mTile.setState(tile.getState());
157 public void onDialogShown() {
158 mIsShowingDialog = true;
161 public void onDialogHidden() {
162 mIsShowingDialog = false;
164 if (DEBUG) Log.d(TAG, "Removing token");
165 mWindowManager.removeWindowToken(mToken);
166 } catch (RemoteException e) {
171 public void setListening(boolean listening) {
172 if (mListening == listening) return;
173 mListening = listening;
178 if (!mServiceManager.isActiveTile()) {
179 mServiceManager.setBindRequested(true);
180 mService.onStartListening();
183 mService.onStopListening();
184 if (mIsTokenGranted && !mIsShowingDialog) {
186 if (DEBUG) Log.d(TAG, "Removing token");
187 mWindowManager.removeWindowToken(mToken);
188 } catch (RemoteException e) {
190 mIsTokenGranted = false;
192 mIsShowingDialog = false;
193 mServiceManager.setBindRequested(false);
195 } catch (RemoteException e) {
196 // Called through wrapper, won't happen here.
201 protected void handleDestroy() {
202 super.handleDestroy();
203 if (mIsTokenGranted) {
205 if (DEBUG) Log.d(TAG, "Removing token");
206 mWindowManager.removeWindowToken(mToken);
207 } catch (RemoteException e) {
210 mHost.getTileServices().freeService(this, mServiceManager);
214 public State newTileState() {
219 public Intent getLongClickIntent() {
220 Intent i = new Intent(TileService.ACTION_QS_TILE_PREFERENCES);
221 i.setPackage(mComponent.getPackageName());
222 i = resolveIntent(i);
226 return new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
227 Uri.fromParts("package", mComponent.getPackageName(), null));
230 private Intent resolveIntent(Intent i) {
231 ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0,
232 ActivityManager.getCurrentUser());
233 return result != null ? new Intent(TileService.ACTION_QS_TILE_PREFERENCES)
234 .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null;
238 protected void handleClick() {
239 if (mTile.getState() == Tile.STATE_UNAVAILABLE) {
243 if (DEBUG) Log.d(TAG, "Adding token");
244 mWindowManager.addWindowToken(mToken, WindowManager.LayoutParams.TYPE_QS_DIALOG);
245 mIsTokenGranted = true;
246 } catch (RemoteException e) {
249 if (mServiceManager.isActiveTile()) {
250 mServiceManager.setBindRequested(true);
251 mService.onStartListening();
253 mService.onClick(mToken);
254 } catch (RemoteException e) {
255 // Called through wrapper, won't happen here.
257 MetricsLogger.action(mContext, getMetricsCategory(), mComponent.getPackageName());
261 public CharSequence getTileLabel() {
262 return getState().label;
266 protected void handleUpdateState(State state, Object arg) {
267 int tileState = mTile.getState();
268 if (mServiceManager.hasPendingBind()) {
269 tileState = Tile.STATE_UNAVAILABLE;
273 drawable = mTile.getIcon().loadDrawable(mContext);
274 } catch (Exception e) {
275 Log.w(TAG, "Invalid icon, forcing into unavailable state");
276 tileState = Tile.STATE_UNAVAILABLE;
277 drawable = mDefaultIcon.loadDrawable(mContext);
279 int color = mContext.getColor(getColor(tileState));
280 drawable.setTint(color);
281 state.icon = new DrawableIcon(drawable);
282 state.label = mTile.getLabel();
283 if (tileState == Tile.STATE_UNAVAILABLE) {
284 state.label = new SpannableStringBuilder().append(state.label,
285 new ForegroundColorSpan(color),
286 SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
288 if (mTile.getContentDescription() != null) {
289 state.contentDescription = mTile.getContentDescription();
291 state.contentDescription = state.label;
296 public int getMetricsCategory() {
297 return MetricsEvent.QS_CUSTOM;
300 public void startUnlockAndRun() {
301 mHost.startRunnableDismissingKeyguard(new Runnable() {
305 mService.onUnlockComplete();
306 } catch (RemoteException e) {
312 private static int getColor(int state) {
314 case Tile.STATE_UNAVAILABLE:
315 return R.color.qs_tile_tint_unavailable;
316 case Tile.STATE_INACTIVE:
317 return R.color.qs_tile_tint_inactive;
318 case Tile.STATE_ACTIVE:
319 return R.color.qs_tile_tint_active;
324 public static String toSpec(ComponentName name) {
325 return PREFIX + name.flattenToShortString() + ")";
328 public static ComponentName getComponentFromSpec(String spec) {
329 final String action = spec.substring(PREFIX.length(), spec.length() - 1);
330 if (action.isEmpty()) {
331 throw new IllegalArgumentException("Empty custom tile spec action");
333 return ComponentName.unflattenFromString(action);
336 public static QSTile<?> create(QSTileHost host, String spec) {
337 if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
338 throw new IllegalArgumentException("Bad custom tile spec: " + spec);
340 final String action = spec.substring(PREFIX.length(), spec.length() - 1);
341 if (action.isEmpty()) {
342 throw new IllegalArgumentException("Empty custom tile spec action");
344 return new CustomTile(host, action);