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