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