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.FlashlightController;
     43 import com.android.systemui.statusbar.policy.HotspotController;
     44 import com.android.systemui.statusbar.policy.KeyguardMonitor;
     45 import com.android.systemui.statusbar.policy.Listenable;
     46 import com.android.systemui.statusbar.policy.LocationController;
     47 import com.android.systemui.statusbar.policy.NetworkController;
     48 import com.android.systemui.statusbar.policy.RotationLockController;
     49 import com.android.systemui.statusbar.policy.UserInfoController;
     50 import com.android.systemui.statusbar.policy.UserSwitcherController;
     51 import com.android.systemui.statusbar.policy.ZenModeController;
     52 
     53 import java.util.ArrayList;
     54 import java.util.Collection;
     55 import java.util.Objects;
     56 
     57 import 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  */
     66 public 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