• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.ComponentName;
22 import android.content.Context;
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.Handler;
32 import android.os.IBinder;
33 import android.os.Looper;
34 import android.os.RemoteException;
35 import android.provider.Settings;
36 import android.service.quicksettings.IQSTileService;
37 import android.service.quicksettings.Tile;
38 import android.service.quicksettings.TileService;
39 import android.text.TextUtils;
40 import android.text.format.DateUtils;
41 import android.util.Log;
42 import android.view.IWindowManager;
43 import android.view.View;
44 import android.view.WindowManagerGlobal;
45 import android.widget.Switch;
46 
47 import androidx.annotation.NonNull;
48 import androidx.annotation.Nullable;
49 import androidx.annotation.WorkerThread;
50 
51 import com.android.internal.logging.MetricsLogger;
52 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
53 import com.android.systemui.Dependency;
54 import com.android.systemui.dagger.qualifiers.Background;
55 import com.android.systemui.dagger.qualifiers.Main;
56 import com.android.systemui.plugins.ActivityStarter;
57 import com.android.systemui.plugins.FalsingManager;
58 import com.android.systemui.plugins.qs.QSTile.State;
59 import com.android.systemui.plugins.statusbar.StatusBarStateController;
60 import com.android.systemui.qs.QSHost;
61 import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
62 import com.android.systemui.qs.logging.QSLogger;
63 import com.android.systemui.qs.tileimpl.QSTileImpl;
64 
65 import java.util.Objects;
66 
67 import javax.inject.Inject;
68 
69 import dagger.Lazy;
70 
71 public class CustomTile extends QSTileImpl<State> implements TileChangeListener {
72     public static final String PREFIX = "custom(";
73 
74     private static final long CUSTOM_STALE_TIMEOUT = DateUtils.HOUR_IN_MILLIS;
75 
76     private static final boolean DEBUG = false;
77 
78     // We don't want to thrash binding and unbinding if the user opens and closes the panel a lot.
79     // So instead we have a period of waiting.
80     private static final long UNBIND_DELAY = 30000;
81 
82     private final ComponentName mComponent;
83     private final Tile mTile;
84     private final IWindowManager mWindowManager;
85     private final IBinder mToken = new Binder();
86     private final IQSTileService mService;
87     private final TileServiceManager mServiceManager;
88     private final int mUser;
89     private final CustomTileStatePersister mCustomTileStatePersister;
90     private android.graphics.drawable.Icon mDefaultIcon;
91     private CharSequence mDefaultLabel;
92 
93     private final Context mUserContext;
94 
95     private boolean mListening;
96     private boolean mIsTokenGranted;
97     private boolean mIsShowingDialog;
98 
99     private final TileServiceKey mKey;
100 
CustomTile( QSHost host, Looper backgroundLooper, Handler mainHandler, FalsingManager falsingManager, MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, String action, Context userContext, CustomTileStatePersister customTileStatePersister )101     private CustomTile(
102             QSHost host,
103             Looper backgroundLooper,
104             Handler mainHandler,
105             FalsingManager falsingManager,
106             MetricsLogger metricsLogger,
107             StatusBarStateController statusBarStateController,
108             ActivityStarter activityStarter,
109             QSLogger qsLogger,
110             String action,
111             Context userContext,
112             CustomTileStatePersister customTileStatePersister
113     ) {
114         super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
115                 statusBarStateController, activityStarter, qsLogger);
116         mWindowManager = WindowManagerGlobal.getWindowManagerService();
117         mComponent = ComponentName.unflattenFromString(action);
118         mTile = new Tile();
119         mUserContext = userContext;
120         mUser = mUserContext.getUserId();
121         mKey = new TileServiceKey(mComponent, mUser);
122 
123         mServiceManager = host.getTileServices().getTileWrapper(this);
124         mService = mServiceManager.getTileService();
125         mCustomTileStatePersister = customTileStatePersister;
126     }
127 
128     @Override
handleInitialize()129     protected void handleInitialize() {
130         updateDefaultTileAndIcon();
131         if (mServiceManager.isToggleableTile()) {
132             // Replace states with BooleanState
133             resetStates();
134         }
135         mServiceManager.setTileChangeListener(this);
136         if (mServiceManager.isActiveTile()) {
137             Tile t = mCustomTileStatePersister.readState(mKey);
138             if (t != null) {
139                 applyTileState(t, /* overwriteNulls */ false);
140                 mServiceManager.clearPendingBind();
141                 refreshState();
142             }
143         }
144     }
145 
146     @Override
getStaleTimeout()147     protected long getStaleTimeout() {
148         return CUSTOM_STALE_TIMEOUT + DateUtils.MINUTE_IN_MILLIS * mHost.indexOf(getTileSpec());
149     }
150 
updateDefaultTileAndIcon()151     private void updateDefaultTileAndIcon() {
152         try {
153             PackageManager pm = mUserContext.getPackageManager();
154             int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_AWARE;
155             if (isSystemApp(pm)) {
156                 flags |= PackageManager.MATCH_DISABLED_COMPONENTS;
157             }
158 
159             ServiceInfo info = pm.getServiceInfo(mComponent, flags);
160             int icon = info.icon != 0 ? info.icon
161                     : info.applicationInfo.icon;
162             // Update the icon if its not set or is the default icon.
163             boolean updateIcon = mTile.getIcon() == null
164                     || iconEquals(mTile.getIcon(), mDefaultIcon);
165             mDefaultIcon = icon != 0 ? android.graphics.drawable.Icon
166                     .createWithResource(mComponent.getPackageName(), icon) : null;
167             if (updateIcon) {
168                 mTile.setIcon(mDefaultIcon);
169             }
170             // Update the label if there is no label or it is the default label.
171             boolean updateLabel = mTile.getLabel() == null
172                     || TextUtils.equals(mTile.getLabel(), mDefaultLabel);
173             mDefaultLabel = info.loadLabel(pm);
174             if (updateLabel) {
175                 mTile.setLabel(mDefaultLabel);
176             }
177         } catch (PackageManager.NameNotFoundException e) {
178             mDefaultIcon = null;
179             mDefaultLabel = null;
180         }
181     }
182 
isSystemApp(PackageManager pm)183     private boolean isSystemApp(PackageManager pm) throws PackageManager.NameNotFoundException {
184         return pm.getApplicationInfo(mComponent.getPackageName(), 0).isSystemApp();
185     }
186 
187     /**
188      * Compare two icons, only works for resources.
189      */
iconEquals(android.graphics.drawable.Icon icon1, android.graphics.drawable.Icon icon2)190     private boolean iconEquals(android.graphics.drawable.Icon icon1,
191             android.graphics.drawable.Icon icon2) {
192         if (icon1 == icon2) {
193             return true;
194         }
195         if (icon1 == null || icon2 == null) {
196             return false;
197         }
198         if (icon1.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE
199                 || icon2.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE) {
200             return false;
201         }
202         if (icon1.getResId() != icon2.getResId()) {
203             return false;
204         }
205         if (!Objects.equals(icon1.getResPackage(), icon2.getResPackage())) {
206             return false;
207         }
208         return true;
209     }
210 
211     @Override
onTileChanged(ComponentName tile)212     public void onTileChanged(ComponentName tile) {
213         mHandler.post(this::updateDefaultTileAndIcon);
214     }
215 
216     @Override
isAvailable()217     public boolean isAvailable() {
218         return mDefaultIcon != null;
219     }
220 
getUser()221     public int getUser() {
222         return mUser;
223     }
224 
getComponent()225     public ComponentName getComponent() {
226         return mComponent;
227     }
228 
229     @Override
populate(LogMaker logMaker)230     public LogMaker populate(LogMaker logMaker) {
231         return super.populate(logMaker).setComponentName(mComponent);
232     }
233 
getQsTile()234     public Tile getQsTile() {
235         // TODO(b/191145007) Move to background thread safely
236         updateDefaultTileAndIcon();
237         return mTile;
238     }
239 
240     /**
241      * Update state of {@link this#mTile} from a remote {@link TileService}.
242      * @param tile tile populated with state to apply
243      */
updateTileState(Tile tile)244     public void updateTileState(Tile tile) {
245         // This comes from a binder call IQSService.updateQsTile
246         mHandler.post(() -> handleUpdateTileState(tile));
247     }
248 
handleUpdateTileState(Tile tile)249     private void handleUpdateTileState(Tile tile) {
250         applyTileState(tile, /* overwriteNulls */ true);
251         if (mServiceManager.isActiveTile()) {
252             mCustomTileStatePersister.persistState(mKey, tile);
253         }
254     }
255 
256     @WorkerThread
applyTileState(Tile tile, boolean overwriteNulls)257     private void applyTileState(Tile tile, boolean overwriteNulls) {
258         if (tile.getIcon() != null || overwriteNulls) {
259             mTile.setIcon(tile.getIcon());
260         }
261         if (tile.getLabel() != null || overwriteNulls) {
262             mTile.setLabel(tile.getLabel());
263         }
264         if (tile.getSubtitle() != null || overwriteNulls) {
265             mTile.setSubtitle(tile.getSubtitle());
266         }
267         if (tile.getContentDescription() != null || overwriteNulls) {
268             mTile.setContentDescription(tile.getContentDescription());
269         }
270         if (tile.getStateDescription() != null || overwriteNulls) {
271             mTile.setStateDescription(tile.getStateDescription());
272         }
273         mTile.setState(tile.getState());
274     }
275 
onDialogShown()276     public void onDialogShown() {
277         mIsShowingDialog = true;
278     }
279 
onDialogHidden()280     public void onDialogHidden() {
281         mIsShowingDialog = false;
282         try {
283             if (DEBUG) Log.d(TAG, "Removing token");
284             mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
285         } catch (RemoteException e) {
286         }
287     }
288 
289     @Override
handleSetListening(boolean listening)290     public void handleSetListening(boolean listening) {
291         super.handleSetListening(listening);
292         if (mListening == listening) return;
293         mListening = listening;
294 
295         try {
296             if (listening) {
297                 updateDefaultTileAndIcon();
298                 refreshState();
299                 if (!mServiceManager.isActiveTile()) {
300                     mServiceManager.setBindRequested(true);
301                     mService.onStartListening();
302                 }
303             } else {
304                 mService.onStopListening();
305                 if (mIsTokenGranted && !mIsShowingDialog) {
306                     try {
307                         if (DEBUG) Log.d(TAG, "Removing token");
308                         mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
309                     } catch (RemoteException e) {
310                     }
311                     mIsTokenGranted = false;
312                 }
313                 mIsShowingDialog = false;
314                 mServiceManager.setBindRequested(false);
315             }
316         } catch (RemoteException e) {
317             // Called through wrapper, won't happen here.
318         }
319     }
320 
321     @Override
handleDestroy()322     protected void handleDestroy() {
323         super.handleDestroy();
324         if (mIsTokenGranted) {
325             try {
326                 if (DEBUG) Log.d(TAG, "Removing token");
327                 mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
328             } catch (RemoteException e) {
329             }
330         }
331         mHost.getTileServices().freeService(this, mServiceManager);
332     }
333 
334     @Override
newTileState()335     public State newTileState() {
336         if (mServiceManager != null && mServiceManager.isToggleableTile()) {
337             return new BooleanState();
338         }
339         return new State();
340     }
341 
342     @Override
getLongClickIntent()343     public Intent getLongClickIntent() {
344         Intent i = new Intent(TileService.ACTION_QS_TILE_PREFERENCES);
345         i.setPackage(mComponent.getPackageName());
346         i = resolveIntent(i);
347         if (i != null) {
348             i.putExtra(Intent.EXTRA_COMPONENT_NAME, mComponent);
349             i.putExtra(TileService.EXTRA_STATE, mTile.getState());
350             return i;
351         }
352         return new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
353                 Uri.fromParts("package", mComponent.getPackageName(), null));
354     }
355 
resolveIntent(Intent i)356     private Intent resolveIntent(Intent i) {
357         ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0, mUser);
358         return result != null ? new Intent(TileService.ACTION_QS_TILE_PREFERENCES)
359                 .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null;
360     }
361 
362     @Override
handleClick(@ullable View view)363     protected void handleClick(@Nullable View view) {
364         if (mTile.getState() == Tile.STATE_UNAVAILABLE) {
365             return;
366         }
367         try {
368             if (DEBUG) Log.d(TAG, "Adding token");
369             mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG, DEFAULT_DISPLAY,
370                     null /* options */);
371             mIsTokenGranted = true;
372         } catch (RemoteException e) {
373         }
374         try {
375             if (mServiceManager.isActiveTile()) {
376                 mServiceManager.setBindRequested(true);
377                 mService.onStartListening();
378             }
379             mService.onClick(mToken);
380         } catch (RemoteException e) {
381             // Called through wrapper, won't happen here.
382         }
383     }
384 
385     @Override
getTileLabel()386     public CharSequence getTileLabel() {
387         return getState().label;
388     }
389 
390     @Override
handleUpdateState(State state, Object arg)391     protected void handleUpdateState(State state, Object arg) {
392         int tileState = mTile.getState();
393         if (mServiceManager.hasPendingBind()) {
394             tileState = Tile.STATE_UNAVAILABLE;
395         }
396         state.state = tileState;
397         Drawable drawable;
398         try {
399             drawable = mTile.getIcon().loadDrawable(mUserContext);
400         } catch (Exception e) {
401             Log.w(TAG, "Invalid icon, forcing into unavailable state");
402             state.state = Tile.STATE_UNAVAILABLE;
403             drawable = mDefaultIcon.loadDrawable(mUserContext);
404         }
405 
406         final Drawable drawableF = drawable;
407         state.iconSupplier = () -> {
408             if (drawableF == null) return null;
409             Drawable.ConstantState cs = drawableF.getConstantState();
410             if (cs != null) {
411                 return new DrawableIcon(cs.newDrawable());
412             }
413             return null;
414         };
415         state.label = mTile.getLabel();
416 
417         CharSequence subtitle = mTile.getSubtitle();
418         if (subtitle != null && subtitle.length() > 0) {
419             state.secondaryLabel = subtitle;
420         } else {
421             state.secondaryLabel = null;
422         }
423 
424         if (mTile.getContentDescription() != null) {
425             state.contentDescription = mTile.getContentDescription();
426         } else {
427             state.contentDescription = state.label;
428         }
429 
430         if (mTile.getStateDescription() != null) {
431             state.stateDescription = mTile.getStateDescription();
432         } else {
433             state.stateDescription = null;
434         }
435 
436         if (state instanceof BooleanState) {
437             state.expandedAccessibilityClassName = Switch.class.getName();
438             ((BooleanState) state).value = (state.state == Tile.STATE_ACTIVE);
439         }
440 
441     }
442 
443     @Override
getMetricsCategory()444     public int getMetricsCategory() {
445         return MetricsEvent.QS_CUSTOM;
446     }
447 
448     @Override
getMetricsSpec()449     public final String getMetricsSpec() {
450         return mComponent.getPackageName();
451     }
452 
startUnlockAndRun()453     public void startUnlockAndRun() {
454         Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> {
455             try {
456                 mService.onUnlockComplete();
457             } catch (RemoteException e) {
458             }
459         });
460     }
461 
toSpec(ComponentName name)462     public static String toSpec(ComponentName name) {
463         return PREFIX + name.flattenToShortString() + ")";
464     }
465 
getComponentFromSpec(String spec)466     public static ComponentName getComponentFromSpec(String spec) {
467         final String action = spec.substring(PREFIX.length(), spec.length() - 1);
468         if (action.isEmpty()) {
469             throw new IllegalArgumentException("Empty custom tile spec action");
470         }
471         return ComponentName.unflattenFromString(action);
472     }
473 
getAction(String spec)474     private static String getAction(String spec) {
475         if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
476             throw new IllegalArgumentException("Bad custom tile spec: " + spec);
477         }
478         final String action = spec.substring(PREFIX.length(), spec.length() - 1);
479         if (action.isEmpty()) {
480             throw new IllegalArgumentException("Empty custom tile spec action");
481         }
482         return action;
483     }
484 
485     /**
486      * Create a {@link CustomTile} for a given spec and user.
487      *
488      * @param builder including injected common dependencies.
489      * @param spec as provided by {@link CustomTile#toSpec}
490      * @param userContext context for the user that is creating this tile.
491      * @return a new {@link CustomTile}
492      */
create(Builder builder, String spec, Context userContext)493     public static CustomTile create(Builder builder, String spec, Context userContext) {
494         return builder
495                 .setSpec(spec)
496                 .setUserContext(userContext)
497                 .build();
498     }
499 
500     public static class Builder {
501         final Lazy<QSHost> mQSHostLazy;
502         final Looper mBackgroundLooper;
503         final Handler mMainHandler;
504         private final FalsingManager mFalsingManager;
505         final MetricsLogger mMetricsLogger;
506         final StatusBarStateController mStatusBarStateController;
507         final ActivityStarter mActivityStarter;
508         final QSLogger mQSLogger;
509         final CustomTileStatePersister mCustomTileStatePersister;
510 
511         Context mUserContext;
512         String mSpec = "";
513 
514         @Inject
Builder( Lazy<QSHost> hostLazy, @Background Looper backgroundLooper, @Main Handler mainHandler, FalsingManager falsingManager, MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, CustomTileStatePersister customTileStatePersister )515         public Builder(
516                 Lazy<QSHost> hostLazy,
517                 @Background Looper backgroundLooper,
518                 @Main Handler mainHandler,
519                 FalsingManager falsingManager,
520                 MetricsLogger metricsLogger,
521                 StatusBarStateController statusBarStateController,
522                 ActivityStarter activityStarter,
523                 QSLogger qsLogger,
524                 CustomTileStatePersister customTileStatePersister
525         ) {
526             mQSHostLazy = hostLazy;
527             mBackgroundLooper = backgroundLooper;
528             mMainHandler = mainHandler;
529             mFalsingManager = falsingManager;
530             mMetricsLogger = metricsLogger;
531             mStatusBarStateController = statusBarStateController;
532             mActivityStarter = activityStarter;
533             mQSLogger = qsLogger;
534             mCustomTileStatePersister = customTileStatePersister;
535         }
536 
setSpec(@onNull String spec)537         Builder setSpec(@NonNull String spec) {
538             mSpec = spec;
539             return this;
540         }
541 
setUserContext(@onNull Context userContext)542         Builder setUserContext(@NonNull Context userContext) {
543             mUserContext = userContext;
544             return this;
545         }
546 
build()547         CustomTile build() {
548             if (mUserContext == null) {
549                 throw new NullPointerException("UserContext cannot be null");
550             }
551             String action = getAction(mSpec);
552             return new CustomTile(
553                     mQSHostLazy.get(),
554                     mBackgroundLooper,
555                     mMainHandler,
556                     mFalsingManager,
557                     mMetricsLogger,
558                     mStatusBarStateController,
559                     mActivityStarter,
560                     mQSLogger,
561                     action,
562                     mUserContext,
563                     mCustomTileStatePersister
564             );
565         }
566     }
567 }
568