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