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