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