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.metrics.LogMaker;
26 import android.net.Uri;
27 import android.os.Binder;
28 import android.os.IBinder;
29 import android.os.RemoteException;
30 import android.provider.Settings;
31 import android.service.quicksettings.IQSTileService;
32 import android.service.quicksettings.Tile;
33 import android.service.quicksettings.TileService;
34 import android.util.Log;
35 import android.view.IWindowManager;
36 import android.view.WindowManagerGlobal;
37 import com.android.internal.logging.MetricsLogger;
38 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
39 import com.android.systemui.Dependency;
40 import com.android.systemui.plugins.ActivityStarter;
41 import com.android.systemui.plugins.qs.QSTile;
42 import com.android.systemui.plugins.qs.QSTile.State;
43 import com.android.systemui.qs.tileimpl.QSTileImpl;
44 import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
45 import com.android.systemui.qs.QSTileHost;
46 import libcore.util.Objects;
47 
48 import static android.view.Display.DEFAULT_DISPLAY;
49 import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG;
50 
51 public class CustomTile extends QSTileImpl<State> implements TileChangeListener {
52     public static final String PREFIX = "custom(";
53 
54     private static final boolean DEBUG = false;
55 
56     // We don't want to thrash binding and unbinding if the user opens and closes the panel a lot.
57     // So instead we have a period of waiting.
58     private static final long UNBIND_DELAY = 30000;
59 
60     private final ComponentName mComponent;
61     private final Tile mTile;
62     private final IWindowManager mWindowManager;
63     private final IBinder mToken = new Binder();
64     private final IQSTileService mService;
65     private final TileServiceManager mServiceManager;
66     private final int mUser;
67     private android.graphics.drawable.Icon mDefaultIcon;
68 
69     private boolean mListening;
70     private boolean mBound;
71     private boolean mIsTokenGranted;
72     private boolean mIsShowingDialog;
73 
CustomTile(QSTileHost host, String action)74     private CustomTile(QSTileHost host, String action) {
75         super(host);
76         mWindowManager = WindowManagerGlobal.getWindowManagerService();
77         mComponent = ComponentName.unflattenFromString(action);
78         mTile = new Tile();
79         setTileIcon();
80         mServiceManager = host.getTileServices().getTileWrapper(this);
81         mService = mServiceManager.getTileService();
82         mServiceManager.setTileChangeListener(this);
83         mUser = ActivityManager.getCurrentUser();
84     }
85 
setTileIcon()86     private void setTileIcon() {
87         try {
88             PackageManager pm = mContext.getPackageManager();
89             int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_AWARE;
90             if (isSystemApp(pm)) {
91                 flags |= PackageManager.MATCH_DISABLED_COMPONENTS;
92             }
93             ServiceInfo info = pm.getServiceInfo(mComponent, flags);
94             int icon = info.icon != 0 ? info.icon
95                     : info.applicationInfo.icon;
96             // Update the icon if its not set or is the default icon.
97             boolean updateIcon = mTile.getIcon() == null
98                     || iconEquals(mTile.getIcon(), mDefaultIcon);
99             mDefaultIcon = icon != 0 ? android.graphics.drawable.Icon
100                     .createWithResource(mComponent.getPackageName(), icon) : null;
101             if (updateIcon) {
102                 mTile.setIcon(mDefaultIcon);
103             }
104             // Update the label if there is no label.
105             if (mTile.getLabel() == null) {
106                 mTile.setLabel(info.loadLabel(pm));
107             }
108         } catch (Exception e) {
109             mDefaultIcon = null;
110         }
111     }
112 
isSystemApp(PackageManager pm)113     private boolean isSystemApp(PackageManager pm) throws PackageManager.NameNotFoundException {
114         return pm.getApplicationInfo(mComponent.getPackageName(), 0).isSystemApp();
115     }
116 
117     /**
118      * Compare two icons, only works for resources.
119      */
iconEquals(android.graphics.drawable.Icon icon1, android.graphics.drawable.Icon icon2)120     private boolean iconEquals(android.graphics.drawable.Icon icon1,
121             android.graphics.drawable.Icon icon2) {
122         if (icon1 == icon2) {
123             return true;
124         }
125         if (icon1 == null || icon2 == null) {
126             return false;
127         }
128         if (icon1.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE
129                 || icon2.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE) {
130             return false;
131         }
132         if (icon1.getResId() != icon2.getResId()) {
133             return false;
134         }
135         if (!Objects.equal(icon1.getResPackage(), icon2.getResPackage())) {
136             return false;
137         }
138         return true;
139     }
140 
141     @Override
onTileChanged(ComponentName tile)142     public void onTileChanged(ComponentName tile) {
143         setTileIcon();
144     }
145 
146     @Override
isAvailable()147     public boolean isAvailable() {
148         return mDefaultIcon != null;
149     }
150 
getUser()151     public int getUser() {
152         return mUser;
153     }
154 
getComponent()155     public ComponentName getComponent() {
156         return mComponent;
157     }
158 
159     @Override
populate(LogMaker logMaker)160     public LogMaker populate(LogMaker logMaker) {
161         return super.populate(logMaker).setComponentName(mComponent);
162     }
163 
getQsTile()164     public Tile getQsTile() {
165         return mTile;
166     }
167 
updateState(Tile tile)168     public void updateState(Tile tile) {
169         mTile.setIcon(tile.getIcon());
170         mTile.setLabel(tile.getLabel());
171         mTile.setContentDescription(tile.getContentDescription());
172         mTile.setState(tile.getState());
173     }
174 
onDialogShown()175     public void onDialogShown() {
176         mIsShowingDialog = true;
177     }
178 
onDialogHidden()179     public void onDialogHidden() {
180         mIsShowingDialog = false;
181         try {
182             if (DEBUG) Log.d(TAG, "Removing token");
183             mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
184         } catch (RemoteException e) {
185         }
186     }
187 
188     @Override
setListening(boolean listening)189     public void setListening(boolean listening) {
190         if (mListening == listening) return;
191         mListening = listening;
192         try {
193             if (listening) {
194                 setTileIcon();
195                 refreshState();
196                 if (!mServiceManager.isActiveTile()) {
197                     mServiceManager.setBindRequested(true);
198                     mService.onStartListening();
199                 }
200             } else {
201                 mService.onStopListening();
202                 if (mIsTokenGranted && !mIsShowingDialog) {
203                     try {
204                         if (DEBUG) Log.d(TAG, "Removing token");
205                         mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
206                     } catch (RemoteException e) {
207                     }
208                     mIsTokenGranted = false;
209                 }
210                 mIsShowingDialog = false;
211                 mServiceManager.setBindRequested(false);
212             }
213         } catch (RemoteException e) {
214             // Called through wrapper, won't happen here.
215         }
216     }
217 
218     @Override
handleDestroy()219     protected void handleDestroy() {
220         super.handleDestroy();
221         if (mIsTokenGranted) {
222             try {
223                 if (DEBUG) Log.d(TAG, "Removing token");
224                 mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
225             } catch (RemoteException e) {
226             }
227         }
228         mHost.getTileServices().freeService(this, mServiceManager);
229     }
230 
231     @Override
newTileState()232     public State newTileState() {
233         State state = new State();
234         return state;
235     }
236 
237     @Override
getLongClickIntent()238     public Intent getLongClickIntent() {
239         Intent i = new Intent(TileService.ACTION_QS_TILE_PREFERENCES);
240         i.setPackage(mComponent.getPackageName());
241         i = resolveIntent(i);
242         if (i != null) {
243             i.putExtra(Intent.EXTRA_COMPONENT_NAME, mComponent);
244             i.putExtra(TileService.EXTRA_STATE, mTile.getState());
245             return i;
246         }
247         return new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
248                 Uri.fromParts("package", mComponent.getPackageName(), null));
249     }
250 
resolveIntent(Intent i)251     private Intent resolveIntent(Intent i) {
252         ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0,
253                 ActivityManager.getCurrentUser());
254         return result != null ? new Intent(TileService.ACTION_QS_TILE_PREFERENCES)
255                 .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null;
256     }
257 
258     @Override
handleClick()259     protected void handleClick() {
260         if (mTile.getState() == Tile.STATE_UNAVAILABLE) {
261             return;
262         }
263         try {
264             if (DEBUG) Log.d(TAG, "Adding token");
265             mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG, DEFAULT_DISPLAY);
266             mIsTokenGranted = true;
267         } catch (RemoteException e) {
268         }
269         try {
270             if (mServiceManager.isActiveTile()) {
271                 mServiceManager.setBindRequested(true);
272                 mService.onStartListening();
273             }
274             mService.onClick(mToken);
275         } catch (RemoteException e) {
276             // Called through wrapper, won't happen here.
277         }
278     }
279 
280     @Override
getTileLabel()281     public CharSequence getTileLabel() {
282         return getState().label;
283     }
284 
285     @Override
handleUpdateState(State state, Object arg)286     protected void handleUpdateState(State state, Object arg) {
287         int tileState = mTile.getState();
288         if (mServiceManager.hasPendingBind()) {
289             tileState = Tile.STATE_UNAVAILABLE;
290         }
291         state.state = tileState;
292         Drawable drawable;
293         try {
294             drawable = mTile.getIcon().loadDrawable(mContext);
295         } catch (Exception e) {
296             Log.w(TAG, "Invalid icon, forcing into unavailable state");
297             state.state = Tile.STATE_UNAVAILABLE;
298             drawable = mDefaultIcon.loadDrawable(mContext);
299         }
300         state.icon = new DrawableIcon(drawable);
301         state.label = mTile.getLabel();
302         if (mTile.getContentDescription() != null) {
303             state.contentDescription = mTile.getContentDescription();
304         } else {
305             state.contentDescription = state.label;
306         }
307     }
308 
309     @Override
getMetricsCategory()310     public int getMetricsCategory() {
311         return MetricsEvent.QS_CUSTOM;
312     }
313 
startUnlockAndRun()314     public void startUnlockAndRun() {
315         Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> {
316             try {
317                 mService.onUnlockComplete();
318             } catch (RemoteException e) {
319             }
320         });
321     }
322 
toSpec(ComponentName name)323     public static String toSpec(ComponentName name) {
324         return PREFIX + name.flattenToShortString() + ")";
325     }
326 
getComponentFromSpec(String spec)327     public static ComponentName getComponentFromSpec(String spec) {
328         final String action = spec.substring(PREFIX.length(), spec.length() - 1);
329         if (action.isEmpty()) {
330             throw new IllegalArgumentException("Empty custom tile spec action");
331         }
332         return ComponentName.unflattenFromString(action);
333     }
334 
create(QSTileHost host, String spec)335     public static QSTile create(QSTileHost host, String spec) {
336         if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
337             throw new IllegalArgumentException("Bad custom tile spec: " + spec);
338         }
339         final String action = spec.substring(PREFIX.length(), spec.length() - 1);
340         if (action.isEmpty()) {
341             throw new IllegalArgumentException("Empty custom tile spec action");
342         }
343         return new CustomTile(host, action);
344     }
345 }
346