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