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