1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.qs; 18 19 import android.app.ActivityManager; 20 import android.app.PendingIntent; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.graphics.drawable.Drawable; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.os.Message; 27 import android.util.ArraySet; 28 import android.util.Log; 29 import android.util.SparseArray; 30 import android.view.View; 31 import android.view.ViewGroup; 32 33 import com.android.internal.logging.MetricsLogger; 34 import com.android.internal.logging.MetricsProto.MetricsEvent; 35 import com.android.settingslib.RestrictedLockUtils; 36 import com.android.systemui.qs.QSTile.State; 37 import com.android.systemui.qs.external.TileServices; 38 import com.android.systemui.statusbar.phone.ManagedProfileController; 39 import com.android.systemui.statusbar.policy.BatteryController; 40 import com.android.systemui.statusbar.policy.BluetoothController; 41 import com.android.systemui.statusbar.policy.CastController; 42 import com.android.systemui.statusbar.policy.NightModeController; 43 import com.android.systemui.statusbar.policy.FlashlightController; 44 import com.android.systemui.statusbar.policy.HotspotController; 45 import com.android.systemui.statusbar.policy.KeyguardMonitor; 46 import com.android.systemui.statusbar.policy.Listenable; 47 import com.android.systemui.statusbar.policy.LocationController; 48 import com.android.systemui.statusbar.policy.NetworkController; 49 import com.android.systemui.statusbar.policy.RotationLockController; 50 import com.android.systemui.statusbar.policy.UserInfoController; 51 import com.android.systemui.statusbar.policy.UserSwitcherController; 52 import com.android.systemui.statusbar.policy.ZenModeController; 53 54 import java.util.ArrayList; 55 import java.util.Collection; 56 import java.util.Objects; 57 58 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 59 60 /** 61 * Base quick-settings tile, extend this to create a new tile. 62 * 63 * State management done on a looper provided by the host. Tiles should update state in 64 * handleUpdateState. Callbacks affecting state should use refreshState to trigger another 65 * state update pass on tile looper. 66 */ 67 public abstract class QSTile<TState extends State> { 68 protected final String TAG = "Tile." + getClass().getSimpleName(); 69 protected static final boolean DEBUG = Log.isLoggable("Tile", Log.DEBUG); 70 71 protected final Host mHost; 72 protected final Context mContext; 73 protected final H mHandler; 74 protected final Handler mUiHandler = new Handler(Looper.getMainLooper()); 75 private final ArraySet<Object> mListeners = new ArraySet<>(); 76 77 private final ArrayList<Callback> mCallbacks = new ArrayList<>(); 78 protected TState mState = newTileState(); 79 private TState mTmpState = newTileState(); 80 private boolean mAnnounceNextStateChange; 81 82 private String mTileSpec; 83 84 public abstract TState newTileState(); 85 abstract protected void handleClick(); 86 abstract protected void handleUpdateState(TState state, Object arg); 87 88 /** 89 * Declare the category of this tile. 90 * 91 * Categories are defined in {@link com.android.internal.logging.MetricsProto.MetricsEvent} 92 * by editing frameworks/base/proto/src/metrics_constants.proto. 93 */ 94 abstract public int getMetricsCategory(); 95 96 protected QSTile(Host host) { 97 mHost = host; 98 mContext = host.getContext(); 99 mHandler = new H(host.getLooper()); 100 } 101 102 /** 103 * Adds or removes a listening client for the tile. If the tile has one or more 104 * listening client it will go into the listening state. 105 */ 106 public void setListening(Object listener, boolean listening) { 107 if (listening) { 108 if (mListeners.add(listener) && mListeners.size() == 1) { 109 if (DEBUG) Log.d(TAG, "setListening " + true); 110 mHandler.obtainMessage(H.SET_LISTENING, 1, 0).sendToTarget(); 111 } 112 } else { 113 if (mListeners.remove(listener) && mListeners.size() == 0) { 114 if (DEBUG) Log.d(TAG, "setListening " + false); 115 mHandler.obtainMessage(H.SET_LISTENING, 0, 0).sendToTarget(); 116 } 117 } 118 } 119 120 public String getTileSpec() { 121 return mTileSpec; 122 } 123 124 public void setTileSpec(String tileSpec) { 125 mTileSpec = tileSpec; 126 } 127 128 public Host getHost() { 129 return mHost; 130 } 131 132 public QSIconView createTileView(Context context) { 133 return new QSIconView(context); 134 } 135 136 public DetailAdapter getDetailAdapter() { 137 return null; // optional 138 } 139 140 /** 141 * Is a startup check whether this device currently supports this tile. 142 * Should not be used to conditionally hide tiles. Only checked on tile 143 * creation or whether should be shown in edit screen. 144 */ 145 public boolean isAvailable() { 146 return true; 147 } 148 149 public interface DetailAdapter { 150 CharSequence getTitle(); 151 Boolean getToggleState(); 152 View createDetailView(Context context, View convertView, ViewGroup parent); 153 Intent getSettingsIntent(); 154 void setToggleState(boolean state); 155 int getMetricsCategory(); 156 } 157 158 // safe to call from any thread 159 160 public void addCallback(Callback callback) { 161 mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget(); 162 } 163 164 public void removeCallback(Callback callback) { 165 mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget(); 166 } 167 168 public void removeCallbacks() { 169 mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS); 170 } 171 172 public void click() { 173 mHandler.sendEmptyMessage(H.CLICK); 174 } 175 176 public void secondaryClick() { 177 mHandler.sendEmptyMessage(H.SECONDARY_CLICK); 178 } 179 180 public void longClick() { 181 mHandler.sendEmptyMessage(H.LONG_CLICK); 182 } 183 184 public void showDetail(boolean show) { 185 mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget(); 186 } 187 188 public final void refreshState() { 189 refreshState(null); 190 } 191 192 protected final void refreshState(Object arg) { 193 mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget(); 194 } 195 196 public final void clearState() { 197 mHandler.sendEmptyMessage(H.CLEAR_STATE); 198 } 199 200 public void userSwitch(int newUserId) { 201 mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget(); 202 } 203 204 public void fireToggleStateChanged(boolean state) { 205 mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget(); 206 } 207 208 public void fireScanStateChanged(boolean state) { 209 mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget(); 210 } 211 212 public void destroy() { 213 mHandler.sendEmptyMessage(H.DESTROY); 214 } 215 216 public TState getState() { 217 return mState; 218 } 219 220 public void setDetailListening(boolean listening) { 221 // optional 222 } 223 224 // call only on tile worker looper 225 226 private void handleAddCallback(Callback callback) { 227 mCallbacks.add(callback); 228 callback.onStateChanged(mState); 229 } 230 231 private void handleRemoveCallback(Callback callback) { 232 mCallbacks.remove(callback); 233 } 234 235 private void handleRemoveCallbacks() { 236 mCallbacks.clear(); 237 } 238 239 protected void handleSecondaryClick() { 240 // Default to normal click. 241 handleClick(); 242 } 243 244 protected void handleLongClick() { 245 MetricsLogger.action(mContext, MetricsEvent.ACTION_QS_LONG_PRESS, getTileSpec()); 246 mHost.startActivityDismissingKeyguard(getLongClickIntent()); 247 } 248 249 public abstract Intent getLongClickIntent(); 250 251 protected void handleClearState() { 252 mTmpState = newTileState(); 253 mState = newTileState(); 254 } 255 256 protected void handleRefreshState(Object arg) { 257 handleUpdateState(mTmpState, arg); 258 final boolean changed = mTmpState.copyTo(mState); 259 if (changed) { 260 handleStateChanged(); 261 } 262 } 263 264 private void handleStateChanged() { 265 boolean delayAnnouncement = shouldAnnouncementBeDelayed(); 266 if (mCallbacks.size() != 0) { 267 for (int i = 0; i < mCallbacks.size(); i++) { 268 mCallbacks.get(i).onStateChanged(mState); 269 } 270 if (mAnnounceNextStateChange && !delayAnnouncement) { 271 String announcement = composeChangeAnnouncement(); 272 if (announcement != null) { 273 mCallbacks.get(0).onAnnouncementRequested(announcement); 274 } 275 } 276 } 277 mAnnounceNextStateChange = mAnnounceNextStateChange && delayAnnouncement; 278 } 279 280 protected boolean shouldAnnouncementBeDelayed() { 281 return false; 282 } 283 284 protected String composeChangeAnnouncement() { 285 return null; 286 } 287 288 private void handleShowDetail(boolean show) { 289 for (int i = 0; i < mCallbacks.size(); i++) { 290 mCallbacks.get(i).onShowDetail(show); 291 } 292 } 293 294 private void handleToggleStateChanged(boolean state) { 295 for (int i = 0; i < mCallbacks.size(); i++) { 296 mCallbacks.get(i).onToggleStateChanged(state); 297 } 298 } 299 300 private void handleScanStateChanged(boolean state) { 301 for (int i = 0; i < mCallbacks.size(); i++) { 302 mCallbacks.get(i).onScanStateChanged(state); 303 } 304 } 305 306 protected void handleUserSwitch(int newUserId) { 307 handleRefreshState(null); 308 } 309 310 protected abstract void setListening(boolean listening); 311 312 protected void handleDestroy() { 313 setListening(false); 314 mCallbacks.clear(); 315 } 316 317 protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) { 318 EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(mContext, 319 userRestriction, ActivityManager.getCurrentUser()); 320 if (admin != null && !RestrictedLockUtils.hasBaseUserRestriction(mContext, 321 userRestriction, ActivityManager.getCurrentUser())) { 322 state.disabledByPolicy = true; 323 state.enforcedAdmin = admin; 324 } else { 325 state.disabledByPolicy = false; 326 state.enforcedAdmin = null; 327 } 328 } 329 330 public abstract CharSequence getTileLabel(); 331 332 protected final class H extends Handler { 333 private static final int ADD_CALLBACK = 1; 334 private static final int CLICK = 2; 335 private static final int SECONDARY_CLICK = 3; 336 private static final int LONG_CLICK = 4; 337 private static final int REFRESH_STATE = 5; 338 private static final int SHOW_DETAIL = 6; 339 private static final int USER_SWITCH = 7; 340 private static final int TOGGLE_STATE_CHANGED = 8; 341 private static final int SCAN_STATE_CHANGED = 9; 342 private static final int DESTROY = 10; 343 private static final int CLEAR_STATE = 11; 344 private static final int REMOVE_CALLBACKS = 12; 345 private static final int REMOVE_CALLBACK = 13; 346 private static final int SET_LISTENING = 14; 347 348 private H(Looper looper) { 349 super(looper); 350 } 351 352 @Override 353 public void handleMessage(Message msg) { 354 String name = null; 355 try { 356 if (msg.what == ADD_CALLBACK) { 357 name = "handleAddCallback"; 358 handleAddCallback((QSTile.Callback) msg.obj); 359 } else if (msg.what == REMOVE_CALLBACKS) { 360 name = "handleRemoveCallbacks"; 361 handleRemoveCallbacks(); 362 } else if (msg.what == REMOVE_CALLBACK) { 363 name = "handleRemoveCallback"; 364 handleRemoveCallback((QSTile.Callback) msg.obj); 365 } else if (msg.what == CLICK) { 366 name = "handleClick"; 367 if (mState.disabledByPolicy) { 368 Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( 369 mContext, mState.enforcedAdmin); 370 mHost.startActivityDismissingKeyguard(intent); 371 } else { 372 mAnnounceNextStateChange = true; 373 handleClick(); 374 } 375 } else if (msg.what == SECONDARY_CLICK) { 376 name = "handleSecondaryClick"; 377 handleSecondaryClick(); 378 } else if (msg.what == LONG_CLICK) { 379 name = "handleLongClick"; 380 handleLongClick(); 381 } else if (msg.what == REFRESH_STATE) { 382 name = "handleRefreshState"; 383 handleRefreshState(msg.obj); 384 } else if (msg.what == SHOW_DETAIL) { 385 name = "handleShowDetail"; 386 handleShowDetail(msg.arg1 != 0); 387 } else if (msg.what == USER_SWITCH) { 388 name = "handleUserSwitch"; 389 handleUserSwitch(msg.arg1); 390 } else if (msg.what == TOGGLE_STATE_CHANGED) { 391 name = "handleToggleStateChanged"; 392 handleToggleStateChanged(msg.arg1 != 0); 393 } else if (msg.what == SCAN_STATE_CHANGED) { 394 name = "handleScanStateChanged"; 395 handleScanStateChanged(msg.arg1 != 0); 396 } else if (msg.what == DESTROY) { 397 name = "handleDestroy"; 398 handleDestroy(); 399 } else if (msg.what == CLEAR_STATE) { 400 name = "handleClearState"; 401 handleClearState(); 402 } else if (msg.what == SET_LISTENING) { 403 name = "setListening"; 404 setListening(msg.arg1 != 0); 405 } else { 406 throw new IllegalArgumentException("Unknown msg: " + msg.what); 407 } 408 } catch (Throwable t) { 409 final String error = "Error in " + name; 410 Log.w(TAG, error, t); 411 mHost.warn(error, t); 412 } 413 } 414 } 415 416 public interface Callback { 417 void onStateChanged(State state); 418 void onShowDetail(boolean show); 419 void onToggleStateChanged(boolean state); 420 void onScanStateChanged(boolean state); 421 void onAnnouncementRequested(CharSequence announcement); 422 } 423 424 public interface Host { 425 void startActivityDismissingKeyguard(Intent intent); 426 void startActivityDismissingKeyguard(PendingIntent intent); 427 void startRunnableDismissingKeyguard(Runnable runnable); 428 void warn(String message, Throwable t); 429 void collapsePanels(); 430 void animateToggleQSExpansion(); 431 void openPanels(); 432 Looper getLooper(); 433 Context getContext(); 434 Collection<QSTile<?>> getTiles(); 435 void addCallback(Callback callback); 436 void removeCallback(Callback callback); 437 BluetoothController getBluetoothController(); 438 LocationController getLocationController(); 439 RotationLockController getRotationLockController(); 440 NetworkController getNetworkController(); 441 ZenModeController getZenModeController(); 442 HotspotController getHotspotController(); 443 CastController getCastController(); 444 FlashlightController getFlashlightController(); 445 KeyguardMonitor getKeyguardMonitor(); 446 UserSwitcherController getUserSwitcherController(); 447 UserInfoController getUserInfoController(); 448 BatteryController getBatteryController(); 449 TileServices getTileServices(); 450 NightModeController getNightModeController(); 451 void removeTile(String tileSpec); 452 ManagedProfileController getManagedProfileController(); 453 454 455 public interface Callback { 456 void onTilesChanged(); 457 } 458 } 459 460 public static abstract class Icon { 461 abstract public Drawable getDrawable(Context context); 462 463 public Drawable getInvisibleDrawable(Context context) { 464 return getDrawable(context); 465 } 466 467 @Override 468 public int hashCode() { 469 return Icon.class.hashCode(); 470 } 471 472 public int getPadding() { 473 return 0; 474 } 475 } 476 477 public static class DrawableIcon extends Icon { 478 protected final Drawable mDrawable; 479 480 public DrawableIcon(Drawable drawable) { 481 mDrawable = drawable; 482 } 483 484 @Override 485 public Drawable getDrawable(Context context) { 486 return mDrawable; 487 } 488 489 @Override 490 public Drawable getInvisibleDrawable(Context context) { 491 return mDrawable; 492 } 493 } 494 495 public static class ResourceIcon extends Icon { 496 private static final SparseArray<Icon> ICONS = new SparseArray<Icon>(); 497 498 protected final int mResId; 499 500 private ResourceIcon(int resId) { 501 mResId = resId; 502 } 503 504 public static Icon get(int resId) { 505 Icon icon = ICONS.get(resId); 506 if (icon == null) { 507 icon = new ResourceIcon(resId); 508 ICONS.put(resId, icon); 509 } 510 return icon; 511 } 512 513 @Override 514 public Drawable getDrawable(Context context) { 515 return context.getDrawable(mResId); 516 } 517 518 @Override 519 public Drawable getInvisibleDrawable(Context context) { 520 return context.getDrawable(mResId); 521 } 522 523 @Override 524 public boolean equals(Object o) { 525 return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId; 526 } 527 528 @Override 529 public String toString() { 530 return String.format("ResourceIcon[resId=0x%08x]", mResId); 531 } 532 } 533 534 protected class AnimationIcon extends ResourceIcon { 535 private final int mAnimatedResId; 536 537 public AnimationIcon(int resId, int staticResId) { 538 super(staticResId); 539 mAnimatedResId = resId; 540 } 541 542 @Override 543 public Drawable getDrawable(Context context) { 544 // workaround: get a clean state for every new AVD 545 return context.getDrawable(mAnimatedResId).getConstantState().newDrawable(); 546 } 547 } 548 549 public static class State { 550 public Icon icon; 551 public CharSequence label; 552 public CharSequence contentDescription; 553 public CharSequence dualLabelContentDescription; 554 public CharSequence minimalContentDescription; 555 public boolean autoMirrorDrawable = true; 556 public boolean disabledByPolicy; 557 public EnforcedAdmin enforcedAdmin; 558 public String minimalAccessibilityClassName; 559 public String expandedAccessibilityClassName; 560 561 public boolean copyTo(State other) { 562 if (other == null) throw new IllegalArgumentException(); 563 if (!other.getClass().equals(getClass())) throw new IllegalArgumentException(); 564 final boolean changed = !Objects.equals(other.icon, icon) 565 || !Objects.equals(other.label, label) 566 || !Objects.equals(other.contentDescription, contentDescription) 567 || !Objects.equals(other.autoMirrorDrawable, autoMirrorDrawable) 568 || !Objects.equals(other.dualLabelContentDescription, 569 dualLabelContentDescription) 570 || !Objects.equals(other.minimalContentDescription, 571 minimalContentDescription) 572 || !Objects.equals(other.minimalAccessibilityClassName, 573 minimalAccessibilityClassName) 574 || !Objects.equals(other.expandedAccessibilityClassName, 575 expandedAccessibilityClassName) 576 || !Objects.equals(other.disabledByPolicy, disabledByPolicy) 577 || !Objects.equals(other.enforcedAdmin, enforcedAdmin); 578 other.icon = icon; 579 other.label = label; 580 other.contentDescription = contentDescription; 581 other.dualLabelContentDescription = dualLabelContentDescription; 582 other.minimalContentDescription = minimalContentDescription; 583 other.minimalAccessibilityClassName = minimalAccessibilityClassName; 584 other.expandedAccessibilityClassName = expandedAccessibilityClassName; 585 other.autoMirrorDrawable = autoMirrorDrawable; 586 other.disabledByPolicy = disabledByPolicy; 587 if (enforcedAdmin == null) { 588 other.enforcedAdmin = null; 589 } else if (other.enforcedAdmin == null) { 590 other.enforcedAdmin = new EnforcedAdmin(enforcedAdmin); 591 } else { 592 enforcedAdmin.copyTo(other.enforcedAdmin); 593 } 594 return changed; 595 } 596 597 @Override 598 public String toString() { 599 return toStringBuilder().toString(); 600 } 601 602 protected StringBuilder toStringBuilder() { 603 final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('['); 604 sb.append(",icon=").append(icon); 605 sb.append(",label=").append(label); 606 sb.append(",contentDescription=").append(contentDescription); 607 sb.append(",dualLabelContentDescription=").append(dualLabelContentDescription); 608 sb.append(",minimalContentDescription=").append(minimalContentDescription); 609 sb.append(",minimalAccessibilityClassName=").append(minimalAccessibilityClassName); 610 sb.append(",expandedAccessibilityClassName=").append(expandedAccessibilityClassName); 611 sb.append(",autoMirrorDrawable=").append(autoMirrorDrawable); 612 sb.append(",disabledByPolicy=").append(disabledByPolicy); 613 sb.append(",enforcedAdmin=").append(enforcedAdmin); 614 return sb.append(']'); 615 } 616 } 617 618 public static class BooleanState extends State { 619 public boolean value; 620 621 @Override 622 public boolean copyTo(State other) { 623 final BooleanState o = (BooleanState) other; 624 final boolean changed = super.copyTo(other) || o.value != value; 625 o.value = value; 626 return changed; 627 } 628 629 @Override 630 protected StringBuilder toStringBuilder() { 631 final StringBuilder rt = super.toStringBuilder(); 632 rt.insert(rt.length() - 1, ",value=" + value); 633 return rt; 634 } 635 } 636 637 public static class AirplaneBooleanState extends BooleanState { 638 public boolean isAirplaneMode; 639 640 @Override 641 public boolean copyTo(State other) { 642 final AirplaneBooleanState o = (AirplaneBooleanState) other; 643 final boolean changed = super.copyTo(other) || o.isAirplaneMode != isAirplaneMode; 644 o.isAirplaneMode = isAirplaneMode; 645 return changed; 646 } 647 } 648 649 public static final class SignalState extends BooleanState { 650 public boolean connected; 651 public boolean activityIn; 652 public boolean activityOut; 653 public int overlayIconId; 654 public boolean filter; 655 public boolean isOverlayIconWide; 656 657 @Override 658 public boolean copyTo(State other) { 659 final SignalState o = (SignalState) other; 660 final boolean changed = o.connected != connected || o.activityIn != activityIn 661 || o.activityOut != activityOut 662 || o.overlayIconId != overlayIconId 663 || o.isOverlayIconWide != isOverlayIconWide; 664 o.connected = connected; 665 o.activityIn = activityIn; 666 o.activityOut = activityOut; 667 o.overlayIconId = overlayIconId; 668 o.filter = filter; 669 o.isOverlayIconWide = isOverlayIconWide; 670 return super.copyTo(other) || changed; 671 } 672 673 @Override 674 protected StringBuilder toStringBuilder() { 675 final StringBuilder rt = super.toStringBuilder(); 676 rt.insert(rt.length() - 1, ",connected=" + connected); 677 rt.insert(rt.length() - 1, ",activityIn=" + activityIn); 678 rt.insert(rt.length() - 1, ",activityOut=" + activityOut); 679 rt.insert(rt.length() - 1, ",overlayIconId=" + overlayIconId); 680 rt.insert(rt.length() - 1, ",filter=" + filter); 681 rt.insert(rt.length() - 1, ",wideOverlayIcon=" + isOverlayIconWide); 682 return rt; 683 } 684 } 685 } 686