Home | History | Annotate | Download | only in tileimpl
      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 
     90     public abstract TState newTileState();
     91 
     92     abstract protected void handleClick();
     93 
     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      */
    102     abstract public int getMetricsCategory();
    103 
    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      */
    113     public void setListening(Object listener, boolean listening) {
    114         mHandler.obtainMessage(H.SET_LISTENING, listening ? 1 : 0, 0, listener).sendToTarget();
    115     }
    116 
    117     protected long getStaleTimeout() {
    118         return DEFAULT_STALE_TIMEOUT;
    119     }
    120 
    121     @VisibleForTesting
    122     protected void handleStale() {
    123         setListening(mStaleListener, true);
    124     }
    125 
    126     public String getTileSpec() {
    127         return mTileSpec;
    128     }
    129 
    130     public void setTileSpec(String tileSpec) {
    131         mTileSpec = tileSpec;
    132     }
    133 
    134     public QSHost getHost() {
    135         return mHost;
    136     }
    137 
    138     public QSIconView createTileView(Context context) {
    139         return new QSIconViewImpl(context);
    140     }
    141 
    142     public DetailAdapter getDetailAdapter() {
    143         return null; // optional
    144     }
    145 
    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      */
    155     public boolean isAvailable() {
    156         return true;
    157     }
    158 
    159     // safe to call from any thread
    160 
    161     public void addCallback(Callback callback) {
    162         mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget();
    163     }
    164 
    165     public void removeCallback(Callback callback) {
    166         mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget();
    167     }
    168 
    169     public void removeCallbacks() {
    170         mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS);
    171     }
    172 
    173     public void click() {
    174         mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION)));
    175         mHandler.sendEmptyMessage(H.CLICK);
    176     }
    177 
    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 
    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 
    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 
    202     public void showDetail(boolean show) {
    203         mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget();
    204     }
    205 
    206     public void refreshState() {
    207         refreshState(null);
    208     }
    209 
    210     protected final void refreshState(Object arg) {
    211         mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
    212     }
    213 
    214     public void clearState() {
    215         mHandler.sendEmptyMessage(H.CLEAR_STATE);
    216     }
    217 
    218     public void userSwitch(int newUserId) {
    219         mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget();
    220     }
    221 
    222     public void fireToggleStateChanged(boolean state) {
    223         mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
    224     }
    225 
    226     public void fireScanStateChanged(boolean state) {
    227         mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
    228     }
    229 
    230     public void destroy() {
    231         mHandler.sendEmptyMessage(H.DESTROY);
    232     }
    233 
    234     public TState getState() {
    235         return mState;
    236     }
    237 
    238     public void setDetailListening(boolean listening) {
    239         // optional
    240     }
    241 
    242     // call only on tile worker looper
    243 
    244     private void handleAddCallback(Callback callback) {
    245         mCallbacks.add(callback);
    246         callback.onStateChanged(mState);
    247     }
    248 
    249     private void handleRemoveCallback(Callback callback) {
    250         mCallbacks.remove(callback);
    251     }
    252 
    253     private void handleRemoveCallbacks() {
    254         mCallbacks.clear();
    255     }
    256 
    257     protected void handleSecondaryClick() {
    258         // Default to normal click.
    259         handleClick();
    260     }
    261 
    262     protected void handleLongClick() {
    263         Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(
    264                 getLongClickIntent(), 0);
    265     }
    266 
    267     public abstract Intent getLongClickIntent();
    268 
    269     protected void handleClearState() {
    270         mTmpState = newTileState();
    271         mState = newTileState();
    272     }
    273 
    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 
    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 
    301     protected boolean shouldAnnouncementBeDelayed() {
    302         return false;
    303     }
    304 
    305     protected String composeChangeAnnouncement() {
    306         return null;
    307     }
    308 
    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 
    316     protected boolean isShowingDetail() {
    317         return mShowingDetail;
    318     }
    319 
    320     private void handleToggleStateChanged(boolean state) {
    321         for (int i = 0; i < mCallbacks.size(); i++) {
    322             mCallbacks.get(i).onToggleStateChanged(state);
    323         }
    324     }
    325 
    326     private void handleScanStateChanged(boolean state) {
    327         for (int i = 0; i < mCallbacks.size(); i++) {
    328             mCallbacks.get(i).onScanStateChanged(state);
    329         }
    330     }
    331 
    332     protected void handleUserSwitch(int newUserId) {
    333         handleRefreshState(null);
    334     }
    335 
    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 
    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 
    362     protected abstract void handleSetListening(boolean listening);
    363 
    364     protected void handleDestroy() {
    365         if (mListeners.size() != 0) {
    366             handleSetListening(false);
    367         }
    368         mCallbacks.clear();
    369     }
    370 
    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 
    384     public abstract CharSequence getTileLabel();
    385 
    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
    419         protected H(Looper looper) {
    420             super(looper);
    421         }
    422 
    423         @Override
    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 
    494         public DrawableIcon(Drawable drawable) {
    495             mDrawable = drawable;
    496             mInvisibleDrawable = drawable.getConstantState().newDrawable();
    497         }
    498 
    499         @Override
    500         public Drawable getDrawable(Context context) {
    501             return mDrawable;
    502         }
    503 
    504         @Override
    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 
    513         public DrawableIconWithRes(Drawable drawable, int id) {
    514             super(drawable);
    515             mId = id;
    516         }
    517 
    518         @Override
    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 
    529         private ResourceIcon(int resId) {
    530             mResId = resId;
    531         }
    532 
    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
    543         public Drawable getDrawable(Context context) {
    544             return context.getDrawable(mResId);
    545         }
    546 
    547         @Override
    548         public Drawable getInvisibleDrawable(Context context) {
    549             return context.getDrawable(mResId);
    550         }
    551 
    552         @Override
    553         public boolean equals(Object o) {
    554             return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId;
    555         }
    556 
    557         @Override
    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 
    566         public AnimationIcon(int resId, int staticResId) {
    567             super(staticResId);
    568             mAnimatedResId = resId;
    569         }
    570 
    571         @Override
    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