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.ActivityManager;
20import android.app.PendingIntent;
21import android.content.Context;
22import android.content.Intent;
23import android.graphics.drawable.Drawable;
24import android.os.Handler;
25import android.os.Looper;
26import android.os.Message;
27import android.util.ArraySet;
28import android.util.Log;
29import android.util.SparseArray;
30import android.view.View;
31import android.view.ViewGroup;
32
33import com.android.internal.logging.MetricsLogger;
34import com.android.internal.logging.MetricsProto.MetricsEvent;
35import com.android.settingslib.RestrictedLockUtils;
36import com.android.systemui.qs.QSTile.State;
37import com.android.systemui.qs.external.TileServices;
38import com.android.systemui.statusbar.phone.ManagedProfileController;
39import com.android.systemui.statusbar.policy.BatteryController;
40import com.android.systemui.statusbar.policy.BluetoothController;
41import com.android.systemui.statusbar.policy.CastController;
42import com.android.systemui.statusbar.policy.NightModeController;
43import com.android.systemui.statusbar.policy.FlashlightController;
44import com.android.systemui.statusbar.policy.HotspotController;
45import com.android.systemui.statusbar.policy.KeyguardMonitor;
46import com.android.systemui.statusbar.policy.Listenable;
47import com.android.systemui.statusbar.policy.LocationController;
48import com.android.systemui.statusbar.policy.NetworkController;
49import com.android.systemui.statusbar.policy.RotationLockController;
50import com.android.systemui.statusbar.policy.UserInfoController;
51import com.android.systemui.statusbar.policy.UserSwitcherController;
52import com.android.systemui.statusbar.policy.ZenModeController;
53
54import java.util.ArrayList;
55import java.util.Collection;
56import java.util.Objects;
57
58import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
59
60/**
61 * Base quick-settings tile, extend this to create a new tile.
62 *
63 * State management done on a looper provided by the host.  Tiles should update state in
64 * handleUpdateState.  Callbacks affecting state should use refreshState to trigger another
65 * state update pass on tile looper.
66 */
67public abstract class QSTile<TState extends State> {
68    protected final String TAG = "Tile." + getClass().getSimpleName();
69    protected static final boolean DEBUG = Log.isLoggable("Tile", Log.DEBUG);
70
71    protected final Host mHost;
72    protected final Context mContext;
73    protected final H mHandler;
74    protected final Handler mUiHandler = new Handler(Looper.getMainLooper());
75    private final ArraySet<Object> mListeners = new ArraySet<>();
76
77    private final ArrayList<Callback> mCallbacks = new ArrayList<>();
78    protected TState mState = newTileState();
79    private TState mTmpState = newTileState();
80    private boolean mAnnounceNextStateChange;
81
82    private String mTileSpec;
83
84    public abstract TState newTileState();
85    abstract protected void handleClick();
86    abstract protected void handleUpdateState(TState state, Object arg);
87
88    /**
89     * Declare the category of this tile.
90     *
91     * Categories are defined in {@link com.android.internal.logging.MetricsProto.MetricsEvent}
92     * by editing frameworks/base/proto/src/metrics_constants.proto.
93     */
94    abstract public int getMetricsCategory();
95
96    protected QSTile(Host host) {
97        mHost = host;
98        mContext = host.getContext();
99        mHandler = new H(host.getLooper());
100    }
101
102    /**
103     * Adds or removes a listening client for the tile. If the tile has one or more
104     * listening client it will go into the listening state.
105     */
106    public void setListening(Object listener, boolean listening) {
107        if (listening) {
108            if (mListeners.add(listener) && mListeners.size() == 1) {
109                if (DEBUG) Log.d(TAG, "setListening " + true);
110                mHandler.obtainMessage(H.SET_LISTENING, 1, 0).sendToTarget();
111            }
112        } else {
113            if (mListeners.remove(listener) && mListeners.size() == 0) {
114                if (DEBUG) Log.d(TAG, "setListening " + false);
115                mHandler.obtainMessage(H.SET_LISTENING, 0, 0).sendToTarget();
116            }
117        }
118    }
119
120    public String getTileSpec() {
121        return mTileSpec;
122    }
123
124    public void setTileSpec(String tileSpec) {
125        mTileSpec = tileSpec;
126    }
127
128    public Host getHost() {
129        return mHost;
130    }
131
132    public QSIconView createTileView(Context context) {
133        return new QSIconView(context);
134    }
135
136    public DetailAdapter getDetailAdapter() {
137        return null; // optional
138    }
139
140    /**
141     * Is a startup check whether this device currently supports this tile.
142     * Should not be used to conditionally hide tiles.  Only checked on tile
143     * creation or whether should be shown in edit screen.
144     */
145    public boolean isAvailable() {
146        return true;
147    }
148
149    public interface DetailAdapter {
150        CharSequence getTitle();
151        Boolean getToggleState();
152        View createDetailView(Context context, View convertView, ViewGroup parent);
153        Intent getSettingsIntent();
154        void setToggleState(boolean state);
155        int getMetricsCategory();
156    }
157
158    // safe to call from any thread
159
160    public void addCallback(Callback callback) {
161        mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget();
162    }
163
164    public void removeCallback(Callback callback) {
165        mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget();
166    }
167
168    public void removeCallbacks() {
169        mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS);
170    }
171
172    public void click() {
173        mHandler.sendEmptyMessage(H.CLICK);
174    }
175
176    public void secondaryClick() {
177        mHandler.sendEmptyMessage(H.SECONDARY_CLICK);
178    }
179
180    public void longClick() {
181        mHandler.sendEmptyMessage(H.LONG_CLICK);
182    }
183
184    public void showDetail(boolean show) {
185        mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget();
186    }
187
188    public final void refreshState() {
189        refreshState(null);
190    }
191
192    protected final void refreshState(Object arg) {
193        mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
194    }
195
196    public final void clearState() {
197        mHandler.sendEmptyMessage(H.CLEAR_STATE);
198    }
199
200    public void userSwitch(int newUserId) {
201        mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget();
202    }
203
204    public void fireToggleStateChanged(boolean state) {
205        mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
206    }
207
208    public void fireScanStateChanged(boolean state) {
209        mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
210    }
211
212    public void destroy() {
213        mHandler.sendEmptyMessage(H.DESTROY);
214    }
215
216    public TState getState() {
217        return mState;
218    }
219
220    public void setDetailListening(boolean listening) {
221        // optional
222    }
223
224    // call only on tile worker looper
225
226    private void handleAddCallback(Callback callback) {
227        mCallbacks.add(callback);
228        callback.onStateChanged(mState);
229    }
230
231    private void handleRemoveCallback(Callback callback) {
232        mCallbacks.remove(callback);
233    }
234
235    private void handleRemoveCallbacks() {
236        mCallbacks.clear();
237    }
238
239    protected void handleSecondaryClick() {
240        // Default to normal click.
241        handleClick();
242    }
243
244    protected void handleLongClick() {
245        MetricsLogger.action(mContext, MetricsEvent.ACTION_QS_LONG_PRESS, getTileSpec());
246        mHost.startActivityDismissingKeyguard(getLongClickIntent());
247    }
248
249    public abstract Intent getLongClickIntent();
250
251    protected void handleClearState() {
252        mTmpState = newTileState();
253        mState = newTileState();
254    }
255
256    protected void handleRefreshState(Object arg) {
257        handleUpdateState(mTmpState, arg);
258        final boolean changed = mTmpState.copyTo(mState);
259        if (changed) {
260            handleStateChanged();
261        }
262    }
263
264    private void handleStateChanged() {
265        boolean delayAnnouncement = shouldAnnouncementBeDelayed();
266        if (mCallbacks.size() != 0) {
267            for (int i = 0; i < mCallbacks.size(); i++) {
268                mCallbacks.get(i).onStateChanged(mState);
269            }
270            if (mAnnounceNextStateChange && !delayAnnouncement) {
271                String announcement = composeChangeAnnouncement();
272                if (announcement != null) {
273                    mCallbacks.get(0).onAnnouncementRequested(announcement);
274                }
275            }
276        }
277        mAnnounceNextStateChange = mAnnounceNextStateChange && delayAnnouncement;
278    }
279
280    protected boolean shouldAnnouncementBeDelayed() {
281        return false;
282    }
283
284    protected String composeChangeAnnouncement() {
285        return null;
286    }
287
288    private void handleShowDetail(boolean show) {
289        for (int i = 0; i < mCallbacks.size(); i++) {
290            mCallbacks.get(i).onShowDetail(show);
291        }
292    }
293
294    private void handleToggleStateChanged(boolean state) {
295        for (int i = 0; i < mCallbacks.size(); i++) {
296            mCallbacks.get(i).onToggleStateChanged(state);
297        }
298    }
299
300    private void handleScanStateChanged(boolean state) {
301        for (int i = 0; i < mCallbacks.size(); i++) {
302            mCallbacks.get(i).onScanStateChanged(state);
303        }
304    }
305
306    protected void handleUserSwitch(int newUserId) {
307        handleRefreshState(null);
308    }
309
310    protected abstract void setListening(boolean listening);
311
312    protected void handleDestroy() {
313        setListening(false);
314        mCallbacks.clear();
315    }
316
317    protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) {
318        EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(mContext,
319                userRestriction, ActivityManager.getCurrentUser());
320        if (admin != null && !RestrictedLockUtils.hasBaseUserRestriction(mContext,
321                userRestriction, ActivityManager.getCurrentUser())) {
322            state.disabledByPolicy = true;
323            state.enforcedAdmin = admin;
324        } else {
325            state.disabledByPolicy = false;
326            state.enforcedAdmin = null;
327        }
328    }
329
330    public abstract CharSequence getTileLabel();
331
332    protected final class H extends Handler {
333        private static final int ADD_CALLBACK = 1;
334        private static final int CLICK = 2;
335        private static final int SECONDARY_CLICK = 3;
336        private static final int LONG_CLICK = 4;
337        private static final int REFRESH_STATE = 5;
338        private static final int SHOW_DETAIL = 6;
339        private static final int USER_SWITCH = 7;
340        private static final int TOGGLE_STATE_CHANGED = 8;
341        private static final int SCAN_STATE_CHANGED = 9;
342        private static final int DESTROY = 10;
343        private static final int CLEAR_STATE = 11;
344        private static final int REMOVE_CALLBACKS = 12;
345        private static final int REMOVE_CALLBACK = 13;
346        private static final int SET_LISTENING = 14;
347
348        private H(Looper looper) {
349            super(looper);
350        }
351
352        @Override
353        public void handleMessage(Message msg) {
354            String name = null;
355            try {
356                if (msg.what == ADD_CALLBACK) {
357                    name = "handleAddCallback";
358                    handleAddCallback((QSTile.Callback) msg.obj);
359                } else if (msg.what == REMOVE_CALLBACKS) {
360                    name = "handleRemoveCallbacks";
361                    handleRemoveCallbacks();
362                } else if (msg.what == REMOVE_CALLBACK) {
363                    name = "handleRemoveCallback";
364                    handleRemoveCallback((QSTile.Callback) msg.obj);
365                } else if (msg.what == CLICK) {
366                    name = "handleClick";
367                    if (mState.disabledByPolicy) {
368                        Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
369                                mContext, mState.enforcedAdmin);
370                        mHost.startActivityDismissingKeyguard(intent);
371                    } else {
372                        mAnnounceNextStateChange = true;
373                        handleClick();
374                    }
375                } else if (msg.what == SECONDARY_CLICK) {
376                    name = "handleSecondaryClick";
377                    handleSecondaryClick();
378                } else if (msg.what == LONG_CLICK) {
379                    name = "handleLongClick";
380                    handleLongClick();
381                } else if (msg.what == REFRESH_STATE) {
382                    name = "handleRefreshState";
383                    handleRefreshState(msg.obj);
384                } else if (msg.what == SHOW_DETAIL) {
385                    name = "handleShowDetail";
386                    handleShowDetail(msg.arg1 != 0);
387                } else if (msg.what == USER_SWITCH) {
388                    name = "handleUserSwitch";
389                    handleUserSwitch(msg.arg1);
390                } else if (msg.what == TOGGLE_STATE_CHANGED) {
391                    name = "handleToggleStateChanged";
392                    handleToggleStateChanged(msg.arg1 != 0);
393                } else if (msg.what == SCAN_STATE_CHANGED) {
394                    name = "handleScanStateChanged";
395                    handleScanStateChanged(msg.arg1 != 0);
396                } else if (msg.what == DESTROY) {
397                    name = "handleDestroy";
398                    handleDestroy();
399                } else if (msg.what == CLEAR_STATE) {
400                    name = "handleClearState";
401                    handleClearState();
402                } else if (msg.what == SET_LISTENING) {
403                    name = "setListening";
404                    setListening(msg.arg1 != 0);
405                } else {
406                    throw new IllegalArgumentException("Unknown msg: " + msg.what);
407                }
408            } catch (Throwable t) {
409                final String error = "Error in " + name;
410                Log.w(TAG, error, t);
411                mHost.warn(error, t);
412            }
413        }
414    }
415
416    public interface Callback {
417        void onStateChanged(State state);
418        void onShowDetail(boolean show);
419        void onToggleStateChanged(boolean state);
420        void onScanStateChanged(boolean state);
421        void onAnnouncementRequested(CharSequence announcement);
422    }
423
424    public interface Host {
425        void startActivityDismissingKeyguard(Intent intent);
426        void startActivityDismissingKeyguard(PendingIntent intent);
427        void startRunnableDismissingKeyguard(Runnable runnable);
428        void warn(String message, Throwable t);
429        void collapsePanels();
430        void animateToggleQSExpansion();
431        void openPanels();
432        Looper getLooper();
433        Context getContext();
434        Collection<QSTile<?>> getTiles();
435        void addCallback(Callback callback);
436        void removeCallback(Callback callback);
437        BluetoothController getBluetoothController();
438        LocationController getLocationController();
439        RotationLockController getRotationLockController();
440        NetworkController getNetworkController();
441        ZenModeController getZenModeController();
442        HotspotController getHotspotController();
443        CastController getCastController();
444        FlashlightController getFlashlightController();
445        KeyguardMonitor getKeyguardMonitor();
446        UserSwitcherController getUserSwitcherController();
447        UserInfoController getUserInfoController();
448        BatteryController getBatteryController();
449        TileServices getTileServices();
450        NightModeController getNightModeController();
451        void removeTile(String tileSpec);
452        ManagedProfileController getManagedProfileController();
453
454
455        public interface Callback {
456            void onTilesChanged();
457        }
458    }
459
460    public static abstract class Icon {
461        abstract public Drawable getDrawable(Context context);
462
463        public Drawable getInvisibleDrawable(Context context) {
464            return getDrawable(context);
465        }
466
467        @Override
468        public int hashCode() {
469            return Icon.class.hashCode();
470        }
471
472        public int getPadding() {
473            return 0;
474        }
475    }
476
477    public static class DrawableIcon extends Icon {
478        protected final Drawable mDrawable;
479
480        public DrawableIcon(Drawable drawable) {
481            mDrawable = drawable;
482        }
483
484        @Override
485        public Drawable getDrawable(Context context) {
486            return mDrawable;
487        }
488
489        @Override
490        public Drawable getInvisibleDrawable(Context context) {
491            return mDrawable;
492        }
493    }
494
495    public static class ResourceIcon extends Icon {
496        private static final SparseArray<Icon> ICONS = new SparseArray<Icon>();
497
498        protected final int mResId;
499
500        private ResourceIcon(int resId) {
501            mResId = resId;
502        }
503
504        public static Icon get(int resId) {
505            Icon icon = ICONS.get(resId);
506            if (icon == null) {
507                icon = new ResourceIcon(resId);
508                ICONS.put(resId, icon);
509            }
510            return icon;
511        }
512
513        @Override
514        public Drawable getDrawable(Context context) {
515            return context.getDrawable(mResId);
516        }
517
518        @Override
519        public Drawable getInvisibleDrawable(Context context) {
520            return context.getDrawable(mResId);
521        }
522
523        @Override
524        public boolean equals(Object o) {
525            return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId;
526        }
527
528        @Override
529        public String toString() {
530            return String.format("ResourceIcon[resId=0x%08x]", mResId);
531        }
532    }
533
534    protected class AnimationIcon extends ResourceIcon {
535        private final int mAnimatedResId;
536
537        public AnimationIcon(int resId, int staticResId) {
538            super(staticResId);
539            mAnimatedResId = resId;
540        }
541
542        @Override
543        public Drawable getDrawable(Context context) {
544            // workaround: get a clean state for every new AVD
545            return context.getDrawable(mAnimatedResId).getConstantState().newDrawable();
546        }
547    }
548
549    public static class State {
550        public Icon icon;
551        public CharSequence label;
552        public CharSequence contentDescription;
553        public CharSequence dualLabelContentDescription;
554        public CharSequence minimalContentDescription;
555        public boolean autoMirrorDrawable = true;
556        public boolean disabledByPolicy;
557        public EnforcedAdmin enforcedAdmin;
558        public String minimalAccessibilityClassName;
559        public String expandedAccessibilityClassName;
560
561        public boolean copyTo(State other) {
562            if (other == null) throw new IllegalArgumentException();
563            if (!other.getClass().equals(getClass())) throw new IllegalArgumentException();
564            final boolean changed = !Objects.equals(other.icon, icon)
565                    || !Objects.equals(other.label, label)
566                    || !Objects.equals(other.contentDescription, contentDescription)
567                    || !Objects.equals(other.autoMirrorDrawable, autoMirrorDrawable)
568                    || !Objects.equals(other.dualLabelContentDescription,
569                    dualLabelContentDescription)
570                    || !Objects.equals(other.minimalContentDescription,
571                    minimalContentDescription)
572                    || !Objects.equals(other.minimalAccessibilityClassName,
573                    minimalAccessibilityClassName)
574                    || !Objects.equals(other.expandedAccessibilityClassName,
575                    expandedAccessibilityClassName)
576                    || !Objects.equals(other.disabledByPolicy, disabledByPolicy)
577                    || !Objects.equals(other.enforcedAdmin, enforcedAdmin);
578            other.icon = icon;
579            other.label = label;
580            other.contentDescription = contentDescription;
581            other.dualLabelContentDescription = dualLabelContentDescription;
582            other.minimalContentDescription = minimalContentDescription;
583            other.minimalAccessibilityClassName = minimalAccessibilityClassName;
584            other.expandedAccessibilityClassName = expandedAccessibilityClassName;
585            other.autoMirrorDrawable = autoMirrorDrawable;
586            other.disabledByPolicy = disabledByPolicy;
587            if (enforcedAdmin == null) {
588                other.enforcedAdmin = null;
589            } else if (other.enforcedAdmin == null) {
590                other.enforcedAdmin = new EnforcedAdmin(enforcedAdmin);
591            } else {
592                enforcedAdmin.copyTo(other.enforcedAdmin);
593            }
594            return changed;
595        }
596
597        @Override
598        public String toString() {
599            return toStringBuilder().toString();
600        }
601
602        protected StringBuilder toStringBuilder() {
603            final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('[');
604            sb.append(",icon=").append(icon);
605            sb.append(",label=").append(label);
606            sb.append(",contentDescription=").append(contentDescription);
607            sb.append(",dualLabelContentDescription=").append(dualLabelContentDescription);
608            sb.append(",minimalContentDescription=").append(minimalContentDescription);
609            sb.append(",minimalAccessibilityClassName=").append(minimalAccessibilityClassName);
610            sb.append(",expandedAccessibilityClassName=").append(expandedAccessibilityClassName);
611            sb.append(",autoMirrorDrawable=").append(autoMirrorDrawable);
612            sb.append(",disabledByPolicy=").append(disabledByPolicy);
613            sb.append(",enforcedAdmin=").append(enforcedAdmin);
614            return sb.append(']');
615        }
616    }
617
618    public static class BooleanState extends State {
619        public boolean value;
620
621        @Override
622        public boolean copyTo(State other) {
623            final BooleanState o = (BooleanState) other;
624            final boolean changed = super.copyTo(other) || o.value != value;
625            o.value = value;
626            return changed;
627        }
628
629        @Override
630        protected StringBuilder toStringBuilder() {
631            final StringBuilder rt = super.toStringBuilder();
632            rt.insert(rt.length() - 1, ",value=" + value);
633            return rt;
634        }
635    }
636
637    public static class AirplaneBooleanState extends BooleanState {
638        public boolean isAirplaneMode;
639
640        @Override
641        public boolean copyTo(State other) {
642            final AirplaneBooleanState o = (AirplaneBooleanState) other;
643            final boolean changed = super.copyTo(other) || o.isAirplaneMode != isAirplaneMode;
644            o.isAirplaneMode = isAirplaneMode;
645            return changed;
646        }
647    }
648
649    public static final class SignalState extends BooleanState {
650        public boolean connected;
651        public boolean activityIn;
652        public boolean activityOut;
653        public int overlayIconId;
654        public boolean filter;
655        public boolean isOverlayIconWide;
656
657        @Override
658        public boolean copyTo(State other) {
659            final SignalState o = (SignalState) other;
660            final boolean changed = o.connected != connected || o.activityIn != activityIn
661                    || o.activityOut != activityOut
662                    || o.overlayIconId != overlayIconId
663                    || o.isOverlayIconWide != isOverlayIconWide;
664            o.connected = connected;
665            o.activityIn = activityIn;
666            o.activityOut = activityOut;
667            o.overlayIconId = overlayIconId;
668            o.filter = filter;
669            o.isOverlayIconWide = isOverlayIconWide;
670            return super.copyTo(other) || changed;
671        }
672
673        @Override
674        protected StringBuilder toStringBuilder() {
675            final StringBuilder rt = super.toStringBuilder();
676            rt.insert(rt.length() - 1, ",connected=" + connected);
677            rt.insert(rt.length() - 1, ",activityIn=" + activityIn);
678            rt.insert(rt.length() - 1, ",activityOut=" + activityOut);
679            rt.insert(rt.length() - 1, ",overlayIconId=" + overlayIconId);
680            rt.insert(rt.length() - 1, ",filter=" + filter);
681            rt.insert(rt.length() - 1, ",wideOverlayIcon=" + isOverlayIconWide);
682            return rt;
683        }
684    }
685}
686