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