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