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