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