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