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