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