• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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
15  */
16 package com.android.systemui.qs.external;
17 
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;
46 
47 public class CustomTile extends QSTile<QSTile.State> implements TileChangeListener {
48     public static final String PREFIX = "custom(";
49 
50     private static final boolean DEBUG = false;
51 
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;
55 
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;
64 
65     private boolean mListening;
66     private boolean mBound;
67     private boolean mIsTokenGranted;
68     private boolean mIsShowingDialog;
69 
CustomTile(QSTileHost host, String action)70     private CustomTile(QSTileHost host, String action) {
71         super(host);
72         mWindowManager = WindowManagerGlobal.getWindowManagerService();
73         mComponent = ComponentName.unflattenFromString(action);
74         mTile = new Tile();
75         setTileIcon();
76         mServiceManager = host.getTileServices().getTileWrapper(this);
77         mService = mServiceManager.getTileService();
78         mServiceManager.setTileChangeListener(this);
79         mUser = ActivityManager.getCurrentUser();
80     }
81 
setTileIcon()82     private void setTileIcon() {
83         try {
84             PackageManager pm = mContext.getPackageManager();
85             int flags = PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE;
86             if (isSystemApp(pm)) {
87                 flags |= PackageManager.MATCH_DISABLED_COMPONENTS;
88             }
89             ServiceInfo info = pm.getServiceInfo(mComponent, flags);
90             int icon = info.icon != 0 ? info.icon
91                     : info.applicationInfo.icon;
92             // Update the icon if its not set or is the default icon.
93             boolean updateIcon = mTile.getIcon() == null
94                     || iconEquals(mTile.getIcon(), mDefaultIcon);
95             mDefaultIcon = icon != 0 ? android.graphics.drawable.Icon
96                     .createWithResource(mComponent.getPackageName(), icon) : null;
97             if (updateIcon) {
98                 mTile.setIcon(mDefaultIcon);
99             }
100             // Update the label if there is no label.
101             if (mTile.getLabel() == null) {
102                 mTile.setLabel(info.loadLabel(pm));
103             }
104         } catch (Exception e) {
105             mDefaultIcon = null;
106         }
107     }
108 
isSystemApp(PackageManager pm)109     private boolean isSystemApp(PackageManager pm) throws PackageManager.NameNotFoundException {
110         return pm.getApplicationInfo(mComponent.getPackageName(), 0).isSystemApp();
111     }
112 
113     /**
114      * Compare two icons, only works for resources.
115      */
iconEquals(android.graphics.drawable.Icon icon1, android.graphics.drawable.Icon icon2)116     private boolean iconEquals(android.graphics.drawable.Icon icon1,
117             android.graphics.drawable.Icon icon2) {
118         if (icon1 == icon2) {
119             return true;
120         }
121         if (icon1 == null || icon2 == null) {
122             return false;
123         }
124         if (icon1.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE
125                 || icon2.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE) {
126             return false;
127         }
128         if (icon1.getResId() != icon2.getResId()) {
129             return false;
130         }
131         if (!Objects.equal(icon1.getResPackage(), icon2.getResPackage())) {
132             return false;
133         }
134         return true;
135     }
136 
137     @Override
onTileChanged(ComponentName tile)138     public void onTileChanged(ComponentName tile) {
139         setTileIcon();
140     }
141 
142     @Override
isAvailable()143     public boolean isAvailable() {
144         return mDefaultIcon != null;
145     }
146 
getUser()147     public int getUser() {
148         return mUser;
149     }
150 
getComponent()151     public ComponentName getComponent() {
152         return mComponent;
153     }
154 
getQsTile()155     public Tile getQsTile() {
156         return mTile;
157     }
158 
updateState(Tile tile)159     public void updateState(Tile tile) {
160         mTile.setIcon(tile.getIcon());
161         mTile.setLabel(tile.getLabel());
162         mTile.setContentDescription(tile.getContentDescription());
163         mTile.setState(tile.getState());
164     }
165 
onDialogShown()166     public void onDialogShown() {
167         mIsShowingDialog = true;
168     }
169 
onDialogHidden()170     public void onDialogHidden() {
171         mIsShowingDialog = false;
172         try {
173             if (DEBUG) Log.d(TAG, "Removing token");
174             mWindowManager.removeWindowToken(mToken);
175         } catch (RemoteException e) {
176         }
177     }
178 
179     @Override
setListening(boolean listening)180     public void setListening(boolean listening) {
181         if (mListening == listening) return;
182         mListening = listening;
183         try {
184             if (listening) {
185                 setTileIcon();
186                 refreshState();
187                 if (!mServiceManager.isActiveTile()) {
188                     mServiceManager.setBindRequested(true);
189                     mService.onStartListening();
190                 }
191             } else {
192                 mService.onStopListening();
193                 if (mIsTokenGranted && !mIsShowingDialog) {
194                     try {
195                         if (DEBUG) Log.d(TAG, "Removing token");
196                         mWindowManager.removeWindowToken(mToken);
197                     } catch (RemoteException e) {
198                     }
199                     mIsTokenGranted = false;
200                 }
201                 mIsShowingDialog = false;
202                 mServiceManager.setBindRequested(false);
203             }
204         } catch (RemoteException e) {
205             // Called through wrapper, won't happen here.
206         }
207     }
208 
209     @Override
handleDestroy()210     protected void handleDestroy() {
211         super.handleDestroy();
212         if (mIsTokenGranted) {
213             try {
214                 if (DEBUG) Log.d(TAG, "Removing token");
215                 mWindowManager.removeWindowToken(mToken);
216             } catch (RemoteException e) {
217             }
218         }
219         mHost.getTileServices().freeService(this, mServiceManager);
220     }
221 
222     @Override
newTileState()223     public State newTileState() {
224         return new State();
225     }
226 
227     @Override
getLongClickIntent()228     public Intent getLongClickIntent() {
229         Intent i = new Intent(TileService.ACTION_QS_TILE_PREFERENCES);
230         i.setPackage(mComponent.getPackageName());
231         i = resolveIntent(i);
232         if (i != null) {
233             return i;
234         }
235         return new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
236                 Uri.fromParts("package", mComponent.getPackageName(), null));
237     }
238 
resolveIntent(Intent i)239     private Intent resolveIntent(Intent i) {
240         ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0,
241                 ActivityManager.getCurrentUser());
242         return result != null ? new Intent(TileService.ACTION_QS_TILE_PREFERENCES)
243                 .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null;
244     }
245 
246     @Override
handleClick()247     protected void handleClick() {
248         if (mTile.getState() == Tile.STATE_UNAVAILABLE) {
249             return;
250         }
251         try {
252             if (DEBUG) Log.d(TAG, "Adding token");
253             mWindowManager.addWindowToken(mToken, WindowManager.LayoutParams.TYPE_QS_DIALOG);
254             mIsTokenGranted = true;
255         } catch (RemoteException e) {
256         }
257         try {
258             if (mServiceManager.isActiveTile()) {
259                 mServiceManager.setBindRequested(true);
260                 mService.onStartListening();
261             }
262             mService.onClick(mToken);
263         } catch (RemoteException e) {
264             // Called through wrapper, won't happen here.
265         }
266         MetricsLogger.action(mContext, getMetricsCategory(), mComponent.getPackageName());
267     }
268 
269     @Override
getTileLabel()270     public CharSequence getTileLabel() {
271         return getState().label;
272     }
273 
274     @Override
handleUpdateState(State state, Object arg)275     protected void handleUpdateState(State state, Object arg) {
276         int tileState = mTile.getState();
277         if (mServiceManager.hasPendingBind()) {
278             tileState = Tile.STATE_UNAVAILABLE;
279         }
280         Drawable drawable;
281         try {
282             drawable = mTile.getIcon().loadDrawable(mContext);
283         } catch (Exception e) {
284             Log.w(TAG, "Invalid icon, forcing into unavailable state");
285             tileState = Tile.STATE_UNAVAILABLE;
286             drawable = mDefaultIcon.loadDrawable(mContext);
287         }
288         int color = mContext.getColor(getColor(tileState));
289         drawable.setTint(color);
290         state.icon = new DrawableIcon(drawable);
291         state.label = mTile.getLabel();
292         if (tileState == Tile.STATE_UNAVAILABLE) {
293             state.label = new SpannableStringBuilder().append(state.label,
294                     new ForegroundColorSpan(color),
295                     SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
296         }
297         if (mTile.getContentDescription() != null) {
298             state.contentDescription = mTile.getContentDescription();
299         } else {
300             state.contentDescription = state.label;
301         }
302     }
303 
304     @Override
getMetricsCategory()305     public int getMetricsCategory() {
306         return MetricsEvent.QS_CUSTOM;
307     }
308 
startUnlockAndRun()309     public void startUnlockAndRun() {
310         mHost.startRunnableDismissingKeyguard(new Runnable() {
311             @Override
312             public void run() {
313                 try {
314                     mService.onUnlockComplete();
315                 } catch (RemoteException e) {
316                 }
317             }
318         });
319     }
320 
getColor(int state)321     private static int getColor(int state) {
322         switch (state) {
323             case Tile.STATE_UNAVAILABLE:
324                 return R.color.qs_tile_tint_unavailable;
325             case Tile.STATE_INACTIVE:
326                 return R.color.qs_tile_tint_inactive;
327             case Tile.STATE_ACTIVE:
328                 return R.color.qs_tile_tint_active;
329         }
330         return 0;
331     }
332 
toSpec(ComponentName name)333     public static String toSpec(ComponentName name) {
334         return PREFIX + name.flattenToShortString() + ")";
335     }
336 
getComponentFromSpec(String spec)337     public static ComponentName getComponentFromSpec(String spec) {
338         final String action = spec.substring(PREFIX.length(), spec.length() - 1);
339         if (action.isEmpty()) {
340             throw new IllegalArgumentException("Empty custom tile spec action");
341         }
342         return ComponentName.unflattenFromString(action);
343     }
344 
create(QSTileHost host, String spec)345     public static QSTile<?> create(QSTileHost host, String spec) {
346         if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
347             throw new IllegalArgumentException("Bad custom tile spec: " + spec);
348         }
349         final String action = spec.substring(PREFIX.length(), spec.length() - 1);
350         if (action.isEmpty()) {
351             throw new IllegalArgumentException("Empty custom tile spec action");
352         }
353         return new CustomTile(host, action);
354     }
355 }
356