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