Home | History | Annotate | Download | only in qs
      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 
     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