• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.systemui.qs;
18 
19 import android.app.ActivityManager;
20 import android.app.PendingIntent;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.graphics.drawable.Drawable;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.os.Message;
27 import android.util.ArraySet;
28 import android.util.Log;
29 import android.util.SparseArray;
30 import android.view.View;
31 import android.view.ViewGroup;
32 
33 import com.android.internal.logging.MetricsLogger;
34 import com.android.internal.logging.MetricsProto.MetricsEvent;
35 import com.android.settingslib.RestrictedLockUtils;
36 import com.android.systemui.qs.QSTile.State;
37 import com.android.systemui.qs.external.TileServices;
38 import com.android.systemui.statusbar.phone.ManagedProfileController;
39 import com.android.systemui.statusbar.policy.BatteryController;
40 import com.android.systemui.statusbar.policy.BluetoothController;
41 import com.android.systemui.statusbar.policy.CastController;
42 import com.android.systemui.statusbar.policy.NightModeController;
43 import com.android.systemui.statusbar.policy.FlashlightController;
44 import com.android.systemui.statusbar.policy.HotspotController;
45 import com.android.systemui.statusbar.policy.KeyguardMonitor;
46 import com.android.systemui.statusbar.policy.Listenable;
47 import com.android.systemui.statusbar.policy.LocationController;
48 import com.android.systemui.statusbar.policy.NetworkController;
49 import com.android.systemui.statusbar.policy.RotationLockController;
50 import com.android.systemui.statusbar.policy.UserInfoController;
51 import com.android.systemui.statusbar.policy.UserSwitcherController;
52 import com.android.systemui.statusbar.policy.ZenModeController;
53 
54 import java.util.ArrayList;
55 import java.util.Collection;
56 import java.util.Objects;
57 
58 import 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  */
67 public 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 
newTileState()84     public abstract TState newTileState();
handleClick()85     abstract protected void handleClick();
handleUpdateState(TState state, Object arg)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      */
getMetricsCategory()94     abstract public int getMetricsCategory();
95 
QSTile(Host host)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      */
setListening(Object listener, boolean listening)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 
getTileSpec()120     public String getTileSpec() {
121         return mTileSpec;
122     }
123 
setTileSpec(String tileSpec)124     public void setTileSpec(String tileSpec) {
125         mTileSpec = tileSpec;
126     }
127 
getHost()128     public Host getHost() {
129         return mHost;
130     }
131 
createTileView(Context context)132     public QSIconView createTileView(Context context) {
133         return new QSIconView(context);
134     }
135 
getDetailAdapter()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      */
isAvailable()145     public boolean isAvailable() {
146         return true;
147     }
148 
149     public interface DetailAdapter {
getTitle()150         CharSequence getTitle();
getToggleState()151         Boolean getToggleState();
createDetailView(Context context, View convertView, ViewGroup parent)152         View createDetailView(Context context, View convertView, ViewGroup parent);
getSettingsIntent()153         Intent getSettingsIntent();
setToggleState(boolean state)154         void setToggleState(boolean state);
getMetricsCategory()155         int getMetricsCategory();
156     }
157 
158     // safe to call from any thread
159 
addCallback(Callback callback)160     public void addCallback(Callback callback) {
161         mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget();
162     }
163 
removeCallback(Callback callback)164     public void removeCallback(Callback callback) {
165         mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget();
166     }
167 
removeCallbacks()168     public void removeCallbacks() {
169         mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS);
170     }
171 
click()172     public void click() {
173         mHandler.sendEmptyMessage(H.CLICK);
174     }
175 
secondaryClick()176     public void secondaryClick() {
177         mHandler.sendEmptyMessage(H.SECONDARY_CLICK);
178     }
179 
longClick()180     public void longClick() {
181         mHandler.sendEmptyMessage(H.LONG_CLICK);
182     }
183 
showDetail(boolean show)184     public void showDetail(boolean show) {
185         mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget();
186     }
187 
refreshState()188     public final void refreshState() {
189         refreshState(null);
190     }
191 
refreshState(Object arg)192     protected final void refreshState(Object arg) {
193         mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
194     }
195 
clearState()196     public final void clearState() {
197         mHandler.sendEmptyMessage(H.CLEAR_STATE);
198     }
199 
userSwitch(int newUserId)200     public void userSwitch(int newUserId) {
201         mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget();
202     }
203 
fireToggleStateChanged(boolean state)204     public void fireToggleStateChanged(boolean state) {
205         mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
206     }
207 
fireScanStateChanged(boolean state)208     public void fireScanStateChanged(boolean state) {
209         mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget();
210     }
211 
destroy()212     public void destroy() {
213         mHandler.sendEmptyMessage(H.DESTROY);
214     }
215 
getState()216     public TState getState() {
217         return mState;
218     }
219 
setDetailListening(boolean listening)220     public void setDetailListening(boolean listening) {
221         // optional
222     }
223 
224     // call only on tile worker looper
225 
handleAddCallback(Callback callback)226     private void handleAddCallback(Callback callback) {
227         mCallbacks.add(callback);
228         callback.onStateChanged(mState);
229     }
230 
handleRemoveCallback(Callback callback)231     private void handleRemoveCallback(Callback callback) {
232         mCallbacks.remove(callback);
233     }
234 
handleRemoveCallbacks()235     private void handleRemoveCallbacks() {
236         mCallbacks.clear();
237     }
238 
handleSecondaryClick()239     protected void handleSecondaryClick() {
240         // Default to normal click.
241         handleClick();
242     }
243 
handleLongClick()244     protected void handleLongClick() {
245         MetricsLogger.action(mContext, MetricsEvent.ACTION_QS_LONG_PRESS, getTileSpec());
246         mHost.startActivityDismissingKeyguard(getLongClickIntent());
247     }
248 
getLongClickIntent()249     public abstract Intent getLongClickIntent();
250 
handleClearState()251     protected void handleClearState() {
252         mTmpState = newTileState();
253         mState = newTileState();
254     }
255 
handleRefreshState(Object arg)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 
handleStateChanged()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 
shouldAnnouncementBeDelayed()280     protected boolean shouldAnnouncementBeDelayed() {
281         return false;
282     }
283 
composeChangeAnnouncement()284     protected String composeChangeAnnouncement() {
285         return null;
286     }
287 
handleShowDetail(boolean show)288     private void handleShowDetail(boolean show) {
289         for (int i = 0; i < mCallbacks.size(); i++) {
290             mCallbacks.get(i).onShowDetail(show);
291         }
292     }
293 
handleToggleStateChanged(boolean state)294     private void handleToggleStateChanged(boolean state) {
295         for (int i = 0; i < mCallbacks.size(); i++) {
296             mCallbacks.get(i).onToggleStateChanged(state);
297         }
298     }
299 
handleScanStateChanged(boolean state)300     private void handleScanStateChanged(boolean state) {
301         for (int i = 0; i < mCallbacks.size(); i++) {
302             mCallbacks.get(i).onScanStateChanged(state);
303         }
304     }
305 
handleUserSwitch(int newUserId)306     protected void handleUserSwitch(int newUserId) {
307         handleRefreshState(null);
308     }
309 
setListening(boolean listening)310     protected abstract void setListening(boolean listening);
311 
handleDestroy()312     protected void handleDestroy() {
313         setListening(false);
314         mCallbacks.clear();
315     }
316 
checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction)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 
getTileLabel()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 
H(Looper looper)348         private H(Looper looper) {
349             super(looper);
350         }
351 
352         @Override
handleMessage(Message msg)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 {
onStateChanged(State state)417         void onStateChanged(State state);
onShowDetail(boolean show)418         void onShowDetail(boolean show);
onToggleStateChanged(boolean state)419         void onToggleStateChanged(boolean state);
onScanStateChanged(boolean state)420         void onScanStateChanged(boolean state);
onAnnouncementRequested(CharSequence announcement)421         void onAnnouncementRequested(CharSequence announcement);
422     }
423 
424     public interface Host {
startActivityDismissingKeyguard(Intent intent)425         void startActivityDismissingKeyguard(Intent intent);
startActivityDismissingKeyguard(PendingIntent intent)426         void startActivityDismissingKeyguard(PendingIntent intent);
startRunnableDismissingKeyguard(Runnable runnable)427         void startRunnableDismissingKeyguard(Runnable runnable);
warn(String message, Throwable t)428         void warn(String message, Throwable t);
collapsePanels()429         void collapsePanels();
animateToggleQSExpansion()430         void animateToggleQSExpansion();
openPanels()431         void openPanels();
getLooper()432         Looper getLooper();
getContext()433         Context getContext();
getTiles()434         Collection<QSTile<?>> getTiles();
addCallback(Callback callback)435         void addCallback(Callback callback);
removeCallback(Callback callback)436         void removeCallback(Callback callback);
getBluetoothController()437         BluetoothController getBluetoothController();
getLocationController()438         LocationController getLocationController();
getRotationLockController()439         RotationLockController getRotationLockController();
getNetworkController()440         NetworkController getNetworkController();
getZenModeController()441         ZenModeController getZenModeController();
getHotspotController()442         HotspotController getHotspotController();
getCastController()443         CastController getCastController();
getFlashlightController()444         FlashlightController getFlashlightController();
getKeyguardMonitor()445         KeyguardMonitor getKeyguardMonitor();
getUserSwitcherController()446         UserSwitcherController getUserSwitcherController();
getUserInfoController()447         UserInfoController getUserInfoController();
getBatteryController()448         BatteryController getBatteryController();
getTileServices()449         TileServices getTileServices();
getNightModeController()450         NightModeController getNightModeController();
removeTile(String tileSpec)451         void removeTile(String tileSpec);
getManagedProfileController()452         ManagedProfileController getManagedProfileController();
453 
454 
455         public interface Callback {
onTilesChanged()456             void onTilesChanged();
457         }
458     }
459 
460     public static abstract class Icon {
getDrawable(Context context)461         abstract public Drawable getDrawable(Context context);
462 
getInvisibleDrawable(Context context)463         public Drawable getInvisibleDrawable(Context context) {
464             return getDrawable(context);
465         }
466 
467         @Override
hashCode()468         public int hashCode() {
469             return Icon.class.hashCode();
470         }
471 
getPadding()472         public int getPadding() {
473             return 0;
474         }
475     }
476 
477     public static class DrawableIcon extends Icon {
478         protected final Drawable mDrawable;
479 
DrawableIcon(Drawable drawable)480         public DrawableIcon(Drawable drawable) {
481             mDrawable = drawable;
482         }
483 
484         @Override
getDrawable(Context context)485         public Drawable getDrawable(Context context) {
486             return mDrawable;
487         }
488 
489         @Override
getInvisibleDrawable(Context context)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 
ResourceIcon(int resId)500         private ResourceIcon(int resId) {
501             mResId = resId;
502         }
503 
get(int resId)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
getDrawable(Context context)514         public Drawable getDrawable(Context context) {
515             return context.getDrawable(mResId);
516         }
517 
518         @Override
getInvisibleDrawable(Context context)519         public Drawable getInvisibleDrawable(Context context) {
520             return context.getDrawable(mResId);
521         }
522 
523         @Override
equals(Object o)524         public boolean equals(Object o) {
525             return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId;
526         }
527 
528         @Override
toString()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 
AnimationIcon(int resId, int staticResId)537         public AnimationIcon(int resId, int staticResId) {
538             super(staticResId);
539             mAnimatedResId = resId;
540         }
541 
542         @Override
getDrawable(Context context)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 
copyTo(State other)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
toString()598         public String toString() {
599             return toStringBuilder().toString();
600         }
601 
toStringBuilder()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
copyTo(State other)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
toStringBuilder()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
copyTo(State other)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
copyTo(State other)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
toStringBuilder()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