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