1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.systemui.qs.tileimpl;
16 
17 import static androidx.lifecycle.Lifecycle.State.DESTROYED;
18 import static androidx.lifecycle.Lifecycle.State.RESUMED;
19 
20 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_CLICK;
21 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_LONG_PRESS;
22 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_SECONDARY_CLICK;
23 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_IS_FULL_QS;
24 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_POSITION;
25 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_VALUE;
26 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_STATUS_BAR_STATE;
27 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION;
28 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
29 
30 import android.app.ActivityManager;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.graphics.drawable.Drawable;
34 import android.metrics.LogMaker;
35 import android.os.Handler;
36 import android.os.Looper;
37 import android.os.Message;
38 import android.service.quicksettings.Tile;
39 import android.text.format.DateUtils;
40 import android.util.ArraySet;
41 import android.util.Log;
42 import android.util.SparseArray;
43 
44 import androidx.annotation.NonNull;
45 import androidx.lifecycle.Lifecycle;
46 import androidx.lifecycle.LifecycleOwner;
47 import androidx.lifecycle.LifecycleRegistry;
48 
49 import com.android.internal.annotations.VisibleForTesting;
50 import com.android.internal.logging.MetricsLogger;
51 import com.android.settingslib.RestrictedLockUtils;
52 import com.android.settingslib.RestrictedLockUtilsInternal;
53 import com.android.settingslib.Utils;
54 import com.android.systemui.Dependency;
55 import com.android.systemui.Dumpable;
56 import com.android.systemui.Prefs;
57 import com.android.systemui.plugins.ActivityStarter;
58 import com.android.systemui.plugins.qs.DetailAdapter;
59 import com.android.systemui.plugins.qs.QSIconView;
60 import com.android.systemui.plugins.qs.QSTile;
61 import com.android.systemui.plugins.qs.QSTile.State;
62 import com.android.systemui.plugins.statusbar.StatusBarStateController;
63 import com.android.systemui.qs.PagedTileLayout.TilePage;
64 import com.android.systemui.qs.QSHost;
65 import com.android.systemui.qs.QuickStatusBarHeader;
66 
67 import java.io.FileDescriptor;
68 import java.io.PrintWriter;
69 import java.util.ArrayList;
70 
71 /**
72  * Base quick-settings tile, extend this to create a new tile.
73  *
74  * State management done on a looper provided by the host.  Tiles should update state in
75  * handleUpdateState.  Callbacks affecting state should use refreshState to trigger another
76  * state update pass on tile looper.
77  *
78  * @param <TState> see above
79  */
80 public abstract class QSTileImpl<TState extends State> implements QSTile, LifecycleOwner, Dumpable {
81     protected final String TAG = "Tile." + getClass().getSimpleName();
82     protected static final boolean DEBUG = Log.isLoggable("Tile", Log.DEBUG);
83 
84     private static final long DEFAULT_STALE_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS;
85     protected static final Object ARG_SHOW_TRANSIENT_ENABLING = new Object();
86 
87     protected final QSHost mHost;
88     protected final Context mContext;
89     // @NonFinalForTesting
90     protected H mHandler = new H(Dependency.get(Dependency.BG_LOOPER));
91     protected final Handler mUiHandler = new Handler(Looper.getMainLooper());
92     private final ArraySet<Object> mListeners = new ArraySet<>();
93     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
94     private final StatusBarStateController
95             mStatusBarStateController = Dependency.get(StatusBarStateController.class);
96 
97     private final ArrayList<Callback> mCallbacks = new ArrayList<>();
98     private final Object mStaleListener = new Object();
99     protected TState mState = newTileState();
100     private TState mTmpState = newTileState();
101     private boolean mAnnounceNextStateChange;
102 
103     private String mTileSpec;
104     private EnforcedAdmin mEnforcedAdmin;
105     private boolean mShowingDetail;
106     private int mIsFullQs;
107 
108     private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
109 
newTileState()110     public abstract TState newTileState();
111 
handleClick()112     abstract protected void handleClick();
113 
handleUpdateState(TState state, Object arg)114     abstract protected void handleUpdateState(TState state, Object arg);
115 
116     /**
117      * Declare the category of this tile.
118      *
119      * Categories are defined in {@link com.android.internal.logging.nano.MetricsProto.MetricsEvent}
120      * by editing frameworks/base/proto/src/metrics_constants.proto.
121      */
getMetricsCategory()122     abstract public int getMetricsCategory();
123 
QSTileImpl(QSHost host)124     protected QSTileImpl(QSHost host) {
125         mHost = host;
126         mContext = host.getContext();
127     }
128 
129     @NonNull
130     @Override
getLifecycle()131     public Lifecycle getLifecycle() {
132         return mLifecycle;
133     }
134 
135     /**
136      * Adds or removes a listening client for the tile. If the tile has one or more
137      * listening client it will go into the listening state.
138      */
setListening(Object listener, boolean listening)139     public void setListening(Object listener, boolean listening) {
140         mHandler.obtainMessage(H.SET_LISTENING, listening ? 1 : 0, 0, listener).sendToTarget();
141     }
142 
getStaleTimeout()143     protected long getStaleTimeout() {
144         return DEFAULT_STALE_TIMEOUT;
145     }
146 
147     @VisibleForTesting
handleStale()148     protected void handleStale() {
149         setListening(mStaleListener, true);
150     }
151 
getTileSpec()152     public String getTileSpec() {
153         return mTileSpec;
154     }
155 
setTileSpec(String tileSpec)156     public void setTileSpec(String tileSpec) {
157         mTileSpec = tileSpec;
158     }
159 
getHost()160     public QSHost getHost() {
161         return mHost;
162     }
163 
createTileView(Context context)164     public QSIconView createTileView(Context context) {
165         return new QSIconViewImpl(context);
166     }
167 
getDetailAdapter()168     public DetailAdapter getDetailAdapter() {
169         return null; // optional
170     }
171 
createDetailAdapter()172     protected DetailAdapter createDetailAdapter() {
173         throw new UnsupportedOperationException();
174     }
175 
176     /**
177      * Is a startup check whether this device currently supports this tile.
178      * Should not be used to conditionally hide tiles.  Only checked on tile
179      * creation or whether should be shown in edit screen.
180      */
isAvailable()181     public boolean isAvailable() {
182         return true;
183     }
184 
185     // safe to call from any thread
186 
addCallback(Callback callback)187     public void addCallback(Callback callback) {
188         mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget();
189     }
190 
removeCallback(Callback callback)191     public void removeCallback(Callback callback) {
192         mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget();
193     }
194 
removeCallbacks()195     public void removeCallbacks() {
196         mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS);
197     }
198 
click()199     public void click() {
200         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION)
201                 .addTaggedData(FIELD_STATUS_BAR_STATE,
202                         mStatusBarStateController.getState())));
203         mHandler.sendEmptyMessage(H.CLICK);
204     }
205 
secondaryClick()206     public void secondaryClick() {
207         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION)
208                 .addTaggedData(FIELD_STATUS_BAR_STATE,
209                         mStatusBarStateController.getState())));
210         mHandler.sendEmptyMessage(H.SECONDARY_CLICK);
211     }
212 
longClick()213     public void longClick() {
214         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION)
215                 .addTaggedData(FIELD_STATUS_BAR_STATE,
216                         mStatusBarStateController.getState())));
217         mHandler.sendEmptyMessage(H.LONG_CLICK);
218 
219         Prefs.putInt(
220                 mContext,
221                 Prefs.Key.QS_LONG_PRESS_TOOLTIP_SHOWN_COUNT,
222                 QuickStatusBarHeader.MAX_TOOLTIP_SHOWN_COUNT);
223     }
224 
populate(LogMaker logMaker)225     public LogMaker populate(LogMaker logMaker) {
226         if (mState instanceof BooleanState) {
227             logMaker.addTaggedData(FIELD_QS_VALUE, ((BooleanState) mState).value ? 1 : 0);
228         }
229         return logMaker.setSubtype(getMetricsCategory())
230                 .addTaggedData(FIELD_IS_FULL_QS, mIsFullQs)
231                 .addTaggedData(FIELD_QS_POSITION, mHost.indexOf(mTileSpec));
232     }
233 
showDetail(boolean show)234     public void showDetail(boolean show) {
235         mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget();
236     }
237 
refreshState()238     public void refreshState() {
239         refreshState(null);
240     }
241 
refreshState(Object arg)242     protected final void refreshState(Object arg) {
243         mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
244     }
245 
userSwitch(int newUserId)246     public void userSwitch(int newUserId) {
247         mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget();
248     }
249 
fireToggleStateChanged(boolean state)250     public void fireToggleStateChanged(boolean state) {
251         mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
252     }
253 
fireScanStateChanged(boolean state)254     public void fireScanStateChanged(boolean state) {
255         mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
256     }
257 
destroy()258     public void destroy() {
259         mHandler.sendEmptyMessage(H.DESTROY);
260     }
261 
getState()262     public TState getState() {
263         return mState;
264     }
265 
setDetailListening(boolean listening)266     public void setDetailListening(boolean listening) {
267         // optional
268     }
269 
270     // call only on tile worker looper
271 
handleAddCallback(Callback callback)272     private void handleAddCallback(Callback callback) {
273         mCallbacks.add(callback);
274         callback.onStateChanged(mState);
275     }
276 
handleRemoveCallback(Callback callback)277     private void handleRemoveCallback(Callback callback) {
278         mCallbacks.remove(callback);
279     }
280 
handleRemoveCallbacks()281     private void handleRemoveCallbacks() {
282         mCallbacks.clear();
283     }
284 
handleSecondaryClick()285     protected void handleSecondaryClick() {
286         // Default to normal click.
287         handleClick();
288     }
289 
handleLongClick()290     protected void handleLongClick() {
291         Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(
292                 getLongClickIntent(), 0);
293     }
294 
getLongClickIntent()295     public abstract Intent getLongClickIntent();
296 
handleRefreshState(Object arg)297     protected void handleRefreshState(Object arg) {
298         handleUpdateState(mTmpState, arg);
299         final boolean changed = mTmpState.copyTo(mState);
300         if (changed) {
301             handleStateChanged();
302         }
303         mHandler.removeMessages(H.STALE);
304         mHandler.sendEmptyMessageDelayed(H.STALE, getStaleTimeout());
305         setListening(mStaleListener, false);
306     }
307 
handleStateChanged()308     private void handleStateChanged() {
309         boolean delayAnnouncement = shouldAnnouncementBeDelayed();
310         if (mCallbacks.size() != 0) {
311             for (int i = 0; i < mCallbacks.size(); i++) {
312                 mCallbacks.get(i).onStateChanged(mState);
313             }
314             if (mAnnounceNextStateChange && !delayAnnouncement) {
315                 String announcement = composeChangeAnnouncement();
316                 if (announcement != null) {
317                     mCallbacks.get(0).onAnnouncementRequested(announcement);
318                 }
319             }
320         }
321         mAnnounceNextStateChange = mAnnounceNextStateChange && delayAnnouncement;
322     }
323 
shouldAnnouncementBeDelayed()324     protected boolean shouldAnnouncementBeDelayed() {
325         return false;
326     }
327 
composeChangeAnnouncement()328     protected String composeChangeAnnouncement() {
329         return null;
330     }
331 
handleShowDetail(boolean show)332     private void handleShowDetail(boolean show) {
333         mShowingDetail = show;
334         for (int i = 0; i < mCallbacks.size(); i++) {
335             mCallbacks.get(i).onShowDetail(show);
336         }
337     }
338 
isShowingDetail()339     protected boolean isShowingDetail() {
340         return mShowingDetail;
341     }
342 
handleToggleStateChanged(boolean state)343     private void handleToggleStateChanged(boolean state) {
344         for (int i = 0; i < mCallbacks.size(); i++) {
345             mCallbacks.get(i).onToggleStateChanged(state);
346         }
347     }
348 
handleScanStateChanged(boolean state)349     private void handleScanStateChanged(boolean state) {
350         for (int i = 0; i < mCallbacks.size(); i++) {
351             mCallbacks.get(i).onScanStateChanged(state);
352         }
353     }
354 
handleUserSwitch(int newUserId)355     protected void handleUserSwitch(int newUserId) {
356         handleRefreshState(null);
357     }
358 
handleSetListeningInternal(Object listener, boolean listening)359     private void handleSetListeningInternal(Object listener, boolean listening) {
360         if (listening) {
361             if (mListeners.add(listener) && mListeners.size() == 1) {
362                 if (DEBUG) Log.d(TAG, "handleSetListening true");
363                 mLifecycle.markState(RESUMED);
364                 handleSetListening(listening);
365                 refreshState(); // Ensure we get at least one refresh after listening.
366             }
367         } else {
368             if (mListeners.remove(listener) && mListeners.size() == 0) {
369                 if (DEBUG) Log.d(TAG, "handleSetListening false");
370                 mLifecycle.markState(DESTROYED);
371                 handleSetListening(listening);
372             }
373         }
374         updateIsFullQs();
375     }
376 
updateIsFullQs()377     private void updateIsFullQs() {
378         for (Object listener : mListeners) {
379             if (TilePage.class.equals(listener.getClass())) {
380                 mIsFullQs = 1;
381                 return;
382             }
383         }
384         mIsFullQs = 0;
385     }
386 
handleSetListening(boolean listening)387     protected abstract void handleSetListening(boolean listening);
388 
handleDestroy()389     protected void handleDestroy() {
390         if (mListeners.size() != 0) {
391             handleSetListening(false);
392         }
393         mCallbacks.clear();
394     }
395 
checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction)396     protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) {
397         EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
398                 userRestriction, ActivityManager.getCurrentUser());
399         if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
400                 userRestriction, ActivityManager.getCurrentUser())) {
401             state.disabledByPolicy = true;
402             mEnforcedAdmin = admin;
403         } else {
404             state.disabledByPolicy = false;
405             mEnforcedAdmin = null;
406         }
407     }
408 
getTileLabel()409     public abstract CharSequence getTileLabel();
410 
getColorForState(Context context, int state)411     public static int getColorForState(Context context, int state) {
412         switch (state) {
413             case Tile.STATE_UNAVAILABLE:
414                 return Utils.getDisabled(context,
415                         Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary));
416             case Tile.STATE_INACTIVE:
417                 return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary);
418             case Tile.STATE_ACTIVE:
419                 return Utils.getColorAttrDefaultColor(context, android.R.attr.colorPrimary);
420             default:
421                 Log.e("QSTile", "Invalid state " + state);
422                 return 0;
423         }
424     }
425 
426     protected final class H extends Handler {
427         private static final int ADD_CALLBACK = 1;
428         private static final int CLICK = 2;
429         private static final int SECONDARY_CLICK = 3;
430         private static final int LONG_CLICK = 4;
431         private static final int REFRESH_STATE = 5;
432         private static final int SHOW_DETAIL = 6;
433         private static final int USER_SWITCH = 7;
434         private static final int TOGGLE_STATE_CHANGED = 8;
435         private static final int SCAN_STATE_CHANGED = 9;
436         private static final int DESTROY = 10;
437         private static final int REMOVE_CALLBACKS = 11;
438         private static final int REMOVE_CALLBACK = 12;
439         private static final int SET_LISTENING = 13;
440         private static final int STALE = 14;
441 
442         @VisibleForTesting
H(Looper looper)443         protected H(Looper looper) {
444             super(looper);
445         }
446 
447         @Override
handleMessage(Message msg)448         public void handleMessage(Message msg) {
449             String name = null;
450             try {
451                 if (msg.what == ADD_CALLBACK) {
452                     name = "handleAddCallback";
453                     handleAddCallback((QSTile.Callback) msg.obj);
454                 } else if (msg.what == REMOVE_CALLBACKS) {
455                     name = "handleRemoveCallbacks";
456                     handleRemoveCallbacks();
457                 } else if (msg.what == REMOVE_CALLBACK) {
458                     name = "handleRemoveCallback";
459                     handleRemoveCallback((QSTile.Callback) msg.obj);
460                 } else if (msg.what == CLICK) {
461                     name = "handleClick";
462                     if (mState.disabledByPolicy) {
463                         Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
464                                 mContext, mEnforcedAdmin);
465                         Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(
466                                 intent, 0);
467                     } else {
468                         handleClick();
469                     }
470                 } else if (msg.what == SECONDARY_CLICK) {
471                     name = "handleSecondaryClick";
472                     handleSecondaryClick();
473                 } else if (msg.what == LONG_CLICK) {
474                     name = "handleLongClick";
475                     handleLongClick();
476                 } else if (msg.what == REFRESH_STATE) {
477                     name = "handleRefreshState";
478                     handleRefreshState(msg.obj);
479                 } else if (msg.what == SHOW_DETAIL) {
480                     name = "handleShowDetail";
481                     handleShowDetail(msg.arg1 != 0);
482                 } else if (msg.what == USER_SWITCH) {
483                     name = "handleUserSwitch";
484                     handleUserSwitch(msg.arg1);
485                 } else if (msg.what == TOGGLE_STATE_CHANGED) {
486                     name = "handleToggleStateChanged";
487                     handleToggleStateChanged(msg.arg1 != 0);
488                 } else if (msg.what == SCAN_STATE_CHANGED) {
489                     name = "handleScanStateChanged";
490                     handleScanStateChanged(msg.arg1 != 0);
491                 } else if (msg.what == DESTROY) {
492                     name = "handleDestroy";
493                     handleDestroy();
494                 } else if (msg.what == SET_LISTENING) {
495                     name = "handleSetListeningInternal";
496                     handleSetListeningInternal(msg.obj, msg.arg1 != 0);
497                 } else if (msg.what == STALE) {
498                     name = "handleStale";
499                     handleStale();
500                 } else {
501                     throw new IllegalArgumentException("Unknown msg: " + msg.what);
502                 }
503             } catch (Throwable t) {
504                 final String error = "Error in " + name;
505                 Log.w(TAG, error, t);
506                 mHost.warn(error, t);
507             }
508         }
509     }
510 
511     public static class DrawableIcon extends Icon {
512         protected final Drawable mDrawable;
513         protected final Drawable mInvisibleDrawable;
514 
DrawableIcon(Drawable drawable)515         public DrawableIcon(Drawable drawable) {
516             mDrawable = drawable;
517             mInvisibleDrawable = drawable.getConstantState().newDrawable();
518         }
519 
520         @Override
getDrawable(Context context)521         public Drawable getDrawable(Context context) {
522             return mDrawable;
523         }
524 
525         @Override
getInvisibleDrawable(Context context)526         public Drawable getInvisibleDrawable(Context context) {
527             return mInvisibleDrawable;
528         }
529     }
530 
531     public static class DrawableIconWithRes extends DrawableIcon {
532         private final int mId;
533 
DrawableIconWithRes(Drawable drawable, int id)534         public DrawableIconWithRes(Drawable drawable, int id) {
535             super(drawable);
536             mId = id;
537         }
538 
539         @Override
equals(Object o)540         public boolean equals(Object o) {
541             return o instanceof DrawableIconWithRes && ((DrawableIconWithRes) o).mId == mId;
542         }
543     }
544 
545     public static class ResourceIcon extends Icon {
546         private static final SparseArray<Icon> ICONS = new SparseArray<Icon>();
547 
548         protected final int mResId;
549 
ResourceIcon(int resId)550         private ResourceIcon(int resId) {
551             mResId = resId;
552         }
553 
get(int resId)554         public static synchronized Icon get(int resId) {
555             Icon icon = ICONS.get(resId);
556             if (icon == null) {
557                 icon = new ResourceIcon(resId);
558                 ICONS.put(resId, icon);
559             }
560             return icon;
561         }
562 
563         @Override
getDrawable(Context context)564         public Drawable getDrawable(Context context) {
565             return context.getDrawable(mResId);
566         }
567 
568         @Override
getInvisibleDrawable(Context context)569         public Drawable getInvisibleDrawable(Context context) {
570             return context.getDrawable(mResId);
571         }
572 
573         @Override
equals(Object o)574         public boolean equals(Object o) {
575             return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId;
576         }
577 
578         @Override
toString()579         public String toString() {
580             return String.format("ResourceIcon[resId=0x%08x]", mResId);
581         }
582     }
583 
584     protected static class AnimationIcon extends ResourceIcon {
585         private final int mAnimatedResId;
586 
AnimationIcon(int resId, int staticResId)587         public AnimationIcon(int resId, int staticResId) {
588             super(staticResId);
589             mAnimatedResId = resId;
590         }
591 
592         @Override
getDrawable(Context context)593         public Drawable getDrawable(Context context) {
594             // workaround: get a clean state for every new AVD
595             return context.getDrawable(mAnimatedResId).getConstantState().newDrawable();
596         }
597     }
598 
599     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)600     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
601         pw.println(this.getClass().getSimpleName() + ":");
602         pw.print("    "); pw.println(getState().toString());
603     }
604 }
605