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