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