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