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