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