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