QSTile.java revision 62692b22fbb6146613635abea225f4049dae4873
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 17package com.android.systemui.qs; 18 19import android.app.PendingIntent; 20import android.content.Context; 21import android.content.Intent; 22import android.graphics.drawable.Animatable; 23import android.graphics.drawable.AnimatedVectorDrawable; 24import android.graphics.drawable.Drawable; 25import android.os.Handler; 26import android.os.Looper; 27import android.os.Message; 28import android.util.Log; 29import android.util.SparseArray; 30import android.view.View; 31import android.view.ViewGroup; 32 33import com.android.systemui.qs.QSTile.State; 34import com.android.systemui.statusbar.policy.*; 35 36import java.util.Collection; 37import java.util.Objects; 38 39/** 40 * Base quick-settings tile, extend this to create a new tile. 41 * 42 * State management done on a looper provided by the host. Tiles should update state in 43 * handleUpdateState. Callbacks affecting state should use refreshState to trigger another 44 * state update pass on tile looper. 45 */ 46public abstract class QSTile<TState extends State> implements Listenable { 47 protected final String TAG = "QSTile." + getClass().getSimpleName(); 48 protected static final boolean DEBUG = Log.isLoggable("QSTile", Log.DEBUG); 49 50 protected final Host mHost; 51 protected final Context mContext; 52 protected final H mHandler; 53 protected final Handler mUiHandler = new Handler(Looper.getMainLooper()); 54 55 private Callback mCallback; 56 protected TState mState = newTileState(); 57 private TState mTmpState = newTileState(); 58 private boolean mAnnounceNextStateChange; 59 60 private String mTileSpec; 61 62 abstract protected TState newTileState(); 63 abstract protected void handleClick(); 64 abstract protected void handleUpdateState(TState state, Object arg); 65 66 /** 67 * Declare the category of this tile. 68 * 69 * Categories are defined in {@link com.android.internal.logging.MetricsLogger} 70 * or if there is no relevant existing category you may define one in 71 * {@link com.android.systemui.qs.QSTile}. 72 */ 73 abstract public int getMetricsCategory(); 74 75 protected QSTile(Host host) { 76 mHost = host; 77 mContext = host.getContext(); 78 mHandler = new H(host.getLooper()); 79 } 80 81 public String getTileSpec() { 82 return mTileSpec; 83 } 84 85 public void setTileSpec(String tileSpec) { 86 mTileSpec = tileSpec; 87 } 88 89 public int getTileType() { 90 return QSTileView.QS_TYPE_NORMAL; 91 } 92 93 public final boolean supportsDualTargets() { 94 return getTileType() == QSTileView.QS_TYPE_DUAL; 95 } 96 97 public Host getHost() { 98 return mHost; 99 } 100 101 public QSTileView createTileView(Context context) { 102 return new QSTileView(context); 103 } 104 105 public DetailAdapter getDetailAdapter() { 106 return null; // optional 107 } 108 109 public interface DetailAdapter { 110 int getTitle(); 111 Boolean getToggleState(); 112 View createDetailView(Context context, View convertView, ViewGroup parent); 113 Intent getSettingsIntent(); 114 void setToggleState(boolean state); 115 int getMetricsCategory(); 116 } 117 118 // safe to call from any thread 119 120 public void setCallback(Callback callback) { 121 mHandler.obtainMessage(H.SET_CALLBACK, callback).sendToTarget(); 122 } 123 124 public void click() { 125 mHandler.sendEmptyMessage(H.CLICK); 126 } 127 128 public void secondaryClick() { 129 mHandler.sendEmptyMessage(H.SECONDARY_CLICK); 130 } 131 132 public void longClick() { 133 mHandler.sendEmptyMessage(H.LONG_CLICK); 134 } 135 136 public void showDetail(boolean show) { 137 mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget(); 138 } 139 140 protected final void refreshState() { 141 refreshState(null); 142 } 143 144 protected final void refreshState(Object arg) { 145 mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget(); 146 } 147 148 public final void clearState() { 149 mHandler.sendEmptyMessage(H.CLEAR_STATE); 150 } 151 152 public void userSwitch(int newUserId) { 153 mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget(); 154 } 155 156 public void fireToggleStateChanged(boolean state) { 157 mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget(); 158 } 159 160 public void fireScanStateChanged(boolean state) { 161 mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget(); 162 } 163 164 public void destroy() { 165 mHandler.sendEmptyMessage(H.DESTROY); 166 } 167 168 public TState getState() { 169 return mState; 170 } 171 172 public void setDetailListening(boolean listening) { 173 // optional 174 } 175 176 // call only on tile worker looper 177 178 private void handleSetCallback(Callback callback) { 179 mCallback = callback; 180 handleRefreshState(null); 181 } 182 183 protected void handleSecondaryClick() { 184 // optional 185 } 186 187 protected void handleLongClick() { 188 // optional 189 } 190 191 protected void handleClearState() { 192 mTmpState = newTileState(); 193 mState = newTileState(); 194 } 195 196 protected void handleRefreshState(Object arg) { 197 handleUpdateState(mTmpState, arg); 198 final boolean changed = mTmpState.copyTo(mState); 199 if (changed) { 200 handleStateChanged(); 201 } 202 } 203 204 private void handleStateChanged() { 205 boolean delayAnnouncement = shouldAnnouncementBeDelayed(); 206 if (mCallback != null) { 207 mCallback.onStateChanged(mState); 208 if (mAnnounceNextStateChange && !delayAnnouncement) { 209 String announcement = composeChangeAnnouncement(); 210 if (announcement != null) { 211 mCallback.onAnnouncementRequested(announcement); 212 } 213 } 214 } 215 mAnnounceNextStateChange = mAnnounceNextStateChange && delayAnnouncement; 216 } 217 218 protected boolean shouldAnnouncementBeDelayed() { 219 return false; 220 } 221 222 protected String composeChangeAnnouncement() { 223 return null; 224 } 225 226 private void handleShowDetail(boolean show) { 227 if (mCallback != null) { 228 mCallback.onShowDetail(show); 229 } 230 } 231 232 private void handleToggleStateChanged(boolean state) { 233 if (mCallback != null) { 234 mCallback.onToggleStateChanged(state); 235 } 236 } 237 238 private void handleScanStateChanged(boolean state) { 239 if (mCallback != null) { 240 mCallback.onScanStateChanged(state); 241 } 242 } 243 244 protected void handleUserSwitch(int newUserId) { 245 handleRefreshState(null); 246 } 247 248 protected void handleDestroy() { 249 setListening(false); 250 mCallback = null; 251 } 252 253 protected final class H extends Handler { 254 private static final int SET_CALLBACK = 1; 255 private static final int CLICK = 2; 256 private static final int SECONDARY_CLICK = 3; 257 private static final int LONG_CLICK = 4; 258 private static final int REFRESH_STATE = 5; 259 private static final int SHOW_DETAIL = 6; 260 private static final int USER_SWITCH = 7; 261 private static final int TOGGLE_STATE_CHANGED = 8; 262 private static final int SCAN_STATE_CHANGED = 9; 263 private static final int DESTROY = 10; 264 private static final int CLEAR_STATE = 11; 265 266 private H(Looper looper) { 267 super(looper); 268 } 269 270 @Override 271 public void handleMessage(Message msg) { 272 String name = null; 273 try { 274 if (msg.what == SET_CALLBACK) { 275 name = "handleSetCallback"; 276 handleSetCallback((QSTile.Callback)msg.obj); 277 } else if (msg.what == CLICK) { 278 name = "handleClick"; 279 mAnnounceNextStateChange = true; 280 handleClick(); 281 } else if (msg.what == SECONDARY_CLICK) { 282 name = "handleSecondaryClick"; 283 handleSecondaryClick(); 284 } else if (msg.what == LONG_CLICK) { 285 name = "handleLongClick"; 286 handleLongClick(); 287 } else if (msg.what == REFRESH_STATE) { 288 name = "handleRefreshState"; 289 handleRefreshState(msg.obj); 290 } else if (msg.what == SHOW_DETAIL) { 291 name = "handleShowDetail"; 292 handleShowDetail(msg.arg1 != 0); 293 } else if (msg.what == USER_SWITCH) { 294 name = "handleUserSwitch"; 295 handleUserSwitch(msg.arg1); 296 } else if (msg.what == TOGGLE_STATE_CHANGED) { 297 name = "handleToggleStateChanged"; 298 handleToggleStateChanged(msg.arg1 != 0); 299 } else if (msg.what == SCAN_STATE_CHANGED) { 300 name = "handleScanStateChanged"; 301 handleScanStateChanged(msg.arg1 != 0); 302 } else if (msg.what == DESTROY) { 303 name = "handleDestroy"; 304 handleDestroy(); 305 } else if (msg.what == CLEAR_STATE) { 306 name = "handleClearState"; 307 handleClearState(); 308 } else { 309 throw new IllegalArgumentException("Unknown msg: " + msg.what); 310 } 311 } catch (Throwable t) { 312 final String error = "Error in " + name; 313 Log.w(TAG, error, t); 314 mHost.warn(error, t); 315 } 316 } 317 } 318 319 public interface Callback { 320 void onStateChanged(State state); 321 void onShowDetail(boolean show); 322 void onToggleStateChanged(boolean state); 323 void onScanStateChanged(boolean state); 324 void onAnnouncementRequested(CharSequence announcement); 325 } 326 327 public interface Host { 328 void startActivityDismissingKeyguard(Intent intent); 329 void startActivityDismissingKeyguard(PendingIntent intent); 330 void warn(String message, Throwable t); 331 void collapsePanels(); 332 Looper getLooper(); 333 Context getContext(); 334 Collection<QSTile<?>> getTiles(); 335 void setCallback(Callback callback); 336 BluetoothController getBluetoothController(); 337 LocationController getLocationController(); 338 RotationLockController getRotationLockController(); 339 NetworkController getNetworkController(); 340 ZenModeController getZenModeController(); 341 HotspotController getHotspotController(); 342 CastController getCastController(); 343 FlashlightController getFlashlightController(); 344 KeyguardMonitor getKeyguardMonitor(); 345 UserSwitcherController getUserSwitcherController(); 346 UserInfoController getUserInfoController(); 347 BatteryController getBatteryController(); 348 349 public interface Callback { 350 void onTilesChanged(); 351 } 352 } 353 354 public static abstract class Icon { 355 abstract public Drawable getDrawable(Context context); 356 357 @Override 358 public int hashCode() { 359 return Icon.class.hashCode(); 360 } 361 } 362 363 public static class ResourceIcon extends Icon { 364 private static final SparseArray<Icon> ICONS = new SparseArray<Icon>(); 365 366 protected final int mResId; 367 368 private ResourceIcon(int resId) { 369 mResId = resId; 370 } 371 372 public static Icon get(int resId) { 373 Icon icon = ICONS.get(resId); 374 if (icon == null) { 375 icon = new ResourceIcon(resId); 376 ICONS.put(resId, icon); 377 } 378 return icon; 379 } 380 381 @Override 382 public Drawable getDrawable(Context context) { 383 Drawable d = context.getDrawable(mResId); 384 if (d instanceof Animatable) { 385 ((Animatable) d).start(); 386 } 387 return d; 388 } 389 390 @Override 391 public boolean equals(Object o) { 392 return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId; 393 } 394 395 @Override 396 public String toString() { 397 return String.format("ResourceIcon[resId=0x%08x]", mResId); 398 } 399 } 400 401 protected class AnimationIcon extends ResourceIcon { 402 private boolean mAllowAnimation; 403 404 public AnimationIcon(int resId) { 405 super(resId); 406 } 407 408 public void setAllowAnimation(boolean allowAnimation) { 409 mAllowAnimation = allowAnimation; 410 } 411 412 @Override 413 public Drawable getDrawable(Context context) { 414 // workaround: get a clean state for every new AVD 415 final AnimatedVectorDrawable d = (AnimatedVectorDrawable) context.getDrawable(mResId) 416 .getConstantState().newDrawable(); 417 d.start(); 418 if (mAllowAnimation) { 419 mAllowAnimation = false; 420 } else { 421 d.stop(); // skip directly to end state 422 } 423 return d; 424 } 425 } 426 427 protected enum UserBoolean { 428 USER_TRUE(true, true), 429 USER_FALSE(true, false), 430 BACKGROUND_TRUE(false, true), 431 BACKGROUND_FALSE(false, false); 432 public final boolean value; 433 public final boolean userInitiated; 434 private UserBoolean(boolean userInitiated, boolean value) { 435 this.value = value; 436 this.userInitiated = userInitiated; 437 } 438 } 439 440 public static class State { 441 public boolean visible; 442 public Icon icon; 443 public String label; 444 public String contentDescription; 445 public String dualLabelContentDescription; 446 public boolean autoMirrorDrawable = true; 447 448 public boolean copyTo(State other) { 449 if (other == null) throw new IllegalArgumentException(); 450 if (!other.getClass().equals(getClass())) throw new IllegalArgumentException(); 451 final boolean changed = other.visible != visible 452 || !Objects.equals(other.icon, icon) 453 || !Objects.equals(other.label, label) 454 || !Objects.equals(other.contentDescription, contentDescription) 455 || !Objects.equals(other.autoMirrorDrawable, autoMirrorDrawable) 456 || !Objects.equals(other.dualLabelContentDescription, 457 dualLabelContentDescription); 458 other.visible = visible; 459 other.icon = icon; 460 other.label = label; 461 other.contentDescription = contentDescription; 462 other.dualLabelContentDescription = dualLabelContentDescription; 463 other.autoMirrorDrawable = autoMirrorDrawable; 464 return changed; 465 } 466 467 @Override 468 public String toString() { 469 return toStringBuilder().toString(); 470 } 471 472 protected StringBuilder toStringBuilder() { 473 final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('['); 474 sb.append("visible=").append(visible); 475 sb.append(",icon=").append(icon); 476 sb.append(",label=").append(label); 477 sb.append(",contentDescription=").append(contentDescription); 478 sb.append(",dualLabelContentDescription=").append(dualLabelContentDescription); 479 sb.append(",autoMirrorDrawable=").append(autoMirrorDrawable); 480 return sb.append(']'); 481 } 482 } 483 484 public static class BooleanState extends State { 485 public boolean value; 486 487 @Override 488 public boolean copyTo(State other) { 489 final BooleanState o = (BooleanState) other; 490 final boolean changed = super.copyTo(other) || o.value != value; 491 o.value = value; 492 return changed; 493 } 494 495 @Override 496 protected StringBuilder toStringBuilder() { 497 final StringBuilder rt = super.toStringBuilder(); 498 rt.insert(rt.length() - 1, ",value=" + value); 499 return rt; 500 } 501 } 502 503 public static final class SignalState extends State { 504 public boolean enabled; 505 public boolean connected; 506 public boolean activityIn; 507 public boolean activityOut; 508 public int overlayIconId; 509 public boolean filter; 510 public boolean isOverlayIconWide; 511 512 @Override 513 public boolean copyTo(State other) { 514 final SignalState o = (SignalState) other; 515 final boolean changed = o.enabled != enabled 516 || o.connected != connected || o.activityIn != activityIn 517 || o.activityOut != activityOut 518 || o.overlayIconId != overlayIconId 519 || o.isOverlayIconWide != isOverlayIconWide; 520 o.enabled = enabled; 521 o.connected = connected; 522 o.activityIn = activityIn; 523 o.activityOut = activityOut; 524 o.overlayIconId = overlayIconId; 525 o.filter = filter; 526 o.isOverlayIconWide = isOverlayIconWide; 527 return super.copyTo(other) || changed; 528 } 529 530 @Override 531 protected StringBuilder toStringBuilder() { 532 final StringBuilder rt = super.toStringBuilder(); 533 rt.insert(rt.length() - 1, ",enabled=" + enabled); 534 rt.insert(rt.length() - 1, ",connected=" + connected); 535 rt.insert(rt.length() - 1, ",activityIn=" + activityIn); 536 rt.insert(rt.length() - 1, ",activityOut=" + activityOut); 537 rt.insert(rt.length() - 1, ",overlayIconId=" + overlayIconId); 538 rt.insert(rt.length() - 1, ",filter=" + filter); 539 rt.insert(rt.length() - 1, ",wideOverlayIcon=" + isOverlayIconWide); 540 return rt; 541 } 542 } 543} 544