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