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