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