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