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