• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.statusbar.phone;
18 
19 import android.annotation.NonNull;
20 import android.content.Context;
21 import android.os.Handler;
22 import android.telephony.SubscriptionInfo;
23 import android.util.ArraySet;
24 import android.util.Log;
25 
26 import com.android.settingslib.mobile.TelephonyIcons;
27 import com.android.systemui.R;
28 import com.android.systemui.dagger.SysUISingleton;
29 import com.android.systemui.statusbar.connectivity.IconState;
30 import com.android.systemui.statusbar.connectivity.MobileDataIndicators;
31 import com.android.systemui.statusbar.connectivity.NetworkController;
32 import com.android.systemui.statusbar.connectivity.SignalCallback;
33 import com.android.systemui.statusbar.connectivity.WifiIndicators;
34 import com.android.systemui.statusbar.policy.SecurityController;
35 import com.android.systemui.tuner.TunerService;
36 import com.android.systemui.tuner.TunerService.Tunable;
37 import com.android.systemui.util.CarrierConfigTracker;
38 
39 import java.util.ArrayList;
40 import java.util.List;
41 import java.util.Objects;
42 
43 import javax.inject.Inject;
44 
45 /** Controls the signal policies for icons shown in the statusbar. **/
46 @SysUISingleton
47 public class StatusBarSignalPolicy implements SignalCallback,
48         SecurityController.SecurityControllerCallback, Tunable {
49     private static final String TAG = "StatusBarSignalPolicy";
50     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
51 
52     private final String mSlotAirplane;
53     private final String mSlotMobile;
54     private final String mSlotWifi;
55     private final String mSlotEthernet;
56     private final String mSlotVpn;
57     private final String mSlotNoCalling;
58     private final String mSlotCallStrength;
59 
60     private final Context mContext;
61     private final StatusBarIconController mIconController;
62     private final NetworkController mNetworkController;
63     private final SecurityController mSecurityController;
64     private final Handler mHandler = Handler.getMain();
65     private final CarrierConfigTracker mCarrierConfigTracker;
66     private final TunerService mTunerService;
67 
68     private boolean mHideAirplane;
69     private boolean mHideMobile;
70     private boolean mHideWifi;
71     private boolean mHideEthernet;
72     private boolean mActivityEnabled;
73 
74     // Track as little state as possible, and only for padding purposes
75     private boolean mIsAirplaneMode = false;
76     private boolean mIsWifiEnabled = false;
77 
78     private ArrayList<MobileIconState> mMobileStates = new ArrayList<>();
79     private ArrayList<CallIndicatorIconState> mCallIndicatorStates = new ArrayList<>();
80     private WifiIconState mWifiIconState = new WifiIconState();
81     private boolean mInitialized;
82 
83     @Inject
StatusBarSignalPolicy( Context context, StatusBarIconController iconController, CarrierConfigTracker carrierConfigTracker, NetworkController networkController, SecurityController securityController, TunerService tunerService )84     public StatusBarSignalPolicy(
85             Context context,
86             StatusBarIconController iconController,
87             CarrierConfigTracker carrierConfigTracker,
88             NetworkController networkController,
89             SecurityController securityController,
90             TunerService tunerService
91     ) {
92         mContext = context;
93 
94         mIconController = iconController;
95         mCarrierConfigTracker = carrierConfigTracker;
96         mNetworkController = networkController;
97         mSecurityController = securityController;
98         mTunerService = tunerService;
99 
100         mSlotAirplane = mContext.getString(com.android.internal.R.string.status_bar_airplane);
101         mSlotMobile   = mContext.getString(com.android.internal.R.string.status_bar_mobile);
102         mSlotWifi     = mContext.getString(com.android.internal.R.string.status_bar_wifi);
103         mSlotEthernet = mContext.getString(com.android.internal.R.string.status_bar_ethernet);
104         mSlotVpn      = mContext.getString(com.android.internal.R.string.status_bar_vpn);
105         mSlotNoCalling = mContext.getString(com.android.internal.R.string.status_bar_no_calling);
106         mSlotCallStrength =
107                 mContext.getString(com.android.internal.R.string.status_bar_call_strength);
108         mActivityEnabled = mContext.getResources().getBoolean(R.bool.config_showActivity);
109     }
110 
111     /** Call to initilaize and register this classw with the system. */
init()112     public void init() {
113         if (mInitialized) {
114             return;
115         }
116         mInitialized = true;
117         mTunerService.addTunable(this, StatusBarIconController.ICON_HIDE_LIST);
118         mNetworkController.addCallback(this);
119         mSecurityController.addCallback(this);
120     }
121 
destroy()122     public void destroy() {
123         mTunerService.removeTunable(this);
124         mNetworkController.removeCallback(this);
125         mSecurityController.removeCallback(this);
126     }
127 
updateVpn()128     private void updateVpn() {
129         boolean vpnVisible = mSecurityController.isVpnEnabled();
130         int vpnIconId = currentVpnIconId(mSecurityController.isVpnBranded());
131 
132         mIconController.setIcon(mSlotVpn, vpnIconId,
133                 mContext.getResources().getString(R.string.accessibility_vpn_on));
134         mIconController.setIconVisibility(mSlotVpn, vpnVisible);
135     }
136 
currentVpnIconId(boolean isBranded)137     private int currentVpnIconId(boolean isBranded) {
138         return isBranded ? R.drawable.stat_sys_branded_vpn : R.drawable.stat_sys_vpn_ic;
139     }
140 
141     /**
142      * From SecurityController
143      */
144     @Override
onStateChanged()145     public void onStateChanged() {
146         mHandler.post(this::updateVpn);
147     }
148 
149     @Override
onTuningChanged(String key, String newValue)150     public void onTuningChanged(String key, String newValue) {
151         if (!StatusBarIconController.ICON_HIDE_LIST.equals(key)) {
152             return;
153         }
154         ArraySet<String> hideList = StatusBarIconController.getIconHideList(mContext, newValue);
155         boolean hideAirplane = hideList.contains(mSlotAirplane);
156         boolean hideMobile = hideList.contains(mSlotMobile);
157         boolean hideWifi = hideList.contains(mSlotWifi);
158         boolean hideEthernet = hideList.contains(mSlotEthernet);
159 
160         if (hideAirplane != mHideAirplane || hideMobile != mHideMobile
161                 || hideEthernet != mHideEthernet || hideWifi != mHideWifi) {
162             mHideAirplane = hideAirplane;
163             mHideMobile = hideMobile;
164             mHideEthernet = hideEthernet;
165             mHideWifi = hideWifi;
166             // Re-register to get new callbacks.
167             mNetworkController.removeCallback(this);
168             mNetworkController.addCallback(this);
169         }
170     }
171 
172     @Override
setWifiIndicators(@onNull WifiIndicators indicators)173     public void setWifiIndicators(@NonNull WifiIndicators indicators) {
174         if (DEBUG) {
175             Log.d(TAG, "setWifiIndicators: " + indicators);
176         }
177         boolean visible = indicators.statusIcon.visible && !mHideWifi;
178         boolean in = indicators.activityIn && mActivityEnabled && visible;
179         boolean out = indicators.activityOut && mActivityEnabled && visible;
180         mIsWifiEnabled = indicators.enabled;
181 
182         WifiIconState newState = mWifiIconState.copy();
183 
184         if (mWifiIconState.noDefaultNetwork && mWifiIconState.noNetworksAvailable
185                 && !mIsAirplaneMode) {
186             newState.visible = true;
187             newState.resId = R.drawable.ic_qs_no_internet_unavailable;
188         } else if (mWifiIconState.noDefaultNetwork && !mWifiIconState.noNetworksAvailable
189                 && (!mIsAirplaneMode || (mIsAirplaneMode && mIsWifiEnabled))) {
190             newState.visible = true;
191             newState.resId = R.drawable.ic_qs_no_internet_available;
192         } else {
193             newState.visible = visible;
194             newState.resId = indicators.statusIcon.icon;
195             newState.activityIn = in;
196             newState.activityOut = out;
197             newState.contentDescription = indicators.statusIcon.contentDescription;
198             MobileIconState first = getFirstMobileState();
199             newState.signalSpacerVisible = first != null && first.typeId != 0;
200         }
201         newState.slot = mSlotWifi;
202         newState.airplaneSpacerVisible = mIsAirplaneMode;
203         updateWifiIconWithState(newState);
204         mWifiIconState = newState;
205     }
206 
updateShowWifiSignalSpacer(WifiIconState state)207     private void updateShowWifiSignalSpacer(WifiIconState state) {
208         MobileIconState first = getFirstMobileState();
209         state.signalSpacerVisible = first != null && first.typeId != 0;
210     }
211 
updateWifiIconWithState(WifiIconState state)212     private void updateWifiIconWithState(WifiIconState state) {
213         if (DEBUG) Log.d(TAG, "WifiIconState: " + state == null ? "" : state.toString());
214         if (state.visible && state.resId > 0) {
215             mIconController.setWifiIcon(mSlotWifi, state);
216             mIconController.setIconVisibility(mSlotWifi, true);
217         } else {
218             mIconController.setIconVisibility(mSlotWifi, false);
219         }
220     }
221 
222     @Override
setCallIndicator(@onNull IconState statusIcon, int subId)223     public void setCallIndicator(@NonNull IconState statusIcon, int subId) {
224         if (DEBUG) {
225             Log.d(TAG, "setCallIndicator: "
226                     + "statusIcon = " + statusIcon + ","
227                     + "subId = " + subId);
228         }
229         CallIndicatorIconState state = getNoCallingState(subId);
230         if (state == null) {
231             return;
232         }
233         if (statusIcon.icon == R.drawable.ic_qs_no_calling_sms) {
234             state.isNoCalling = statusIcon.visible;
235             state.noCallingDescription = statusIcon.contentDescription;
236         } else {
237             state.callStrengthResId = statusIcon.icon;
238             state.callStrengthDescription = statusIcon.contentDescription;
239         }
240         if (mCarrierConfigTracker.getCallStrengthConfig(subId)) {
241             mIconController.setCallStrengthIcons(mSlotCallStrength,
242                     CallIndicatorIconState.copyStates(mCallIndicatorStates));
243         } else {
244             mIconController.removeIcon(mSlotCallStrength, subId);
245         }
246         mIconController.setNoCallingIcons(mSlotNoCalling,
247                 CallIndicatorIconState.copyStates(mCallIndicatorStates));
248     }
249 
250     @Override
setMobileDataIndicators(@onNull MobileDataIndicators indicators)251     public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) {
252         if (DEBUG) {
253             Log.d(TAG, "setMobileDataIndicators: " + indicators);
254         }
255         MobileIconState state = getState(indicators.subId);
256         if (state == null) {
257             return;
258         }
259 
260         // Visibility of the data type indicator changed
261         boolean typeChanged = indicators.statusType != state.typeId
262                 && (indicators.statusType == 0 || state.typeId == 0);
263 
264         state.visible = indicators.statusIcon.visible && !mHideMobile;
265         state.strengthId = indicators.statusIcon.icon;
266         state.typeId = indicators.statusType;
267         state.contentDescription = indicators.statusIcon.contentDescription;
268         state.typeContentDescription = indicators.typeContentDescription;
269         state.showTriangle = indicators.showTriangle;
270         state.roaming = indicators.roaming;
271         state.activityIn = indicators.activityIn && mActivityEnabled;
272         state.activityOut = indicators.activityOut && mActivityEnabled;
273 
274         if (DEBUG) {
275             Log.d(TAG, "MobileIconStates: "
276                     + (mMobileStates == null ? "" : mMobileStates.toString()));
277         }
278         // Always send a copy to maintain value type semantics
279         mIconController.setMobileIcons(mSlotMobile, MobileIconState.copyStates(mMobileStates));
280 
281         if (typeChanged) {
282             WifiIconState wifiCopy = mWifiIconState.copy();
283             updateShowWifiSignalSpacer(wifiCopy);
284             if (!Objects.equals(wifiCopy, mWifiIconState)) {
285                 updateWifiIconWithState(wifiCopy);
286                 mWifiIconState = wifiCopy;
287             }
288         }
289     }
290 
getNoCallingState(int subId)291     private CallIndicatorIconState getNoCallingState(int subId) {
292         for (CallIndicatorIconState state : mCallIndicatorStates) {
293             if (state.subId == subId) {
294                 return state;
295             }
296         }
297         Log.e(TAG, "Unexpected subscription " + subId);
298         return null;
299     }
300 
getState(int subId)301     private MobileIconState getState(int subId) {
302         for (MobileIconState state : mMobileStates) {
303             if (state.subId == subId) {
304                 return state;
305             }
306         }
307         Log.e(TAG, "Unexpected subscription " + subId);
308         return null;
309     }
310 
getFirstMobileState()311     private MobileIconState getFirstMobileState() {
312         if (mMobileStates.size() > 0) {
313             return mMobileStates.get(0);
314         }
315 
316         return null;
317     }
318 
319 
320     /**
321      * It is expected that a call to setSubs will be immediately followed by setMobileDataIndicators
322      * so we don't have to update the icon manager at this point, just remove the old ones
323      * @param subs list of mobile subscriptions, displayed as mobile data indicators (max 8)
324      */
325     @Override
setSubs(List<SubscriptionInfo> subs)326     public void setSubs(List<SubscriptionInfo> subs) {
327         if (DEBUG) Log.d(TAG, "setSubs: " + (subs == null ? "" : subs.toString()));
328         if (hasCorrectSubs(subs)) {
329             return;
330         }
331 
332         mIconController.removeAllIconsForSlot(mSlotMobile);
333         mIconController.removeAllIconsForSlot(mSlotNoCalling);
334         mIconController.removeAllIconsForSlot(mSlotCallStrength);
335         mMobileStates.clear();
336         List<CallIndicatorIconState> noCallingStates = new ArrayList<CallIndicatorIconState>();
337         noCallingStates.addAll(mCallIndicatorStates);
338         mCallIndicatorStates.clear();
339         final int n = subs.size();
340         for (int i = 0; i < n; i++) {
341             mMobileStates.add(new MobileIconState(subs.get(i).getSubscriptionId()));
342             boolean isNewSub = true;
343             for (CallIndicatorIconState state : noCallingStates) {
344                 if (state.subId == subs.get(i).getSubscriptionId()) {
345                     mCallIndicatorStates.add(state);
346                     isNewSub = false;
347                     break;
348                 }
349             }
350             if (isNewSub) {
351                 mCallIndicatorStates.add(
352                         new CallIndicatorIconState(subs.get(i).getSubscriptionId()));
353             }
354         }
355     }
356 
hasCorrectSubs(List<SubscriptionInfo> subs)357     private boolean hasCorrectSubs(List<SubscriptionInfo> subs) {
358         final int N = subs.size();
359         if (N != mMobileStates.size()) {
360             return false;
361         }
362         for (int i = 0; i < N; i++) {
363             if (mMobileStates.get(i).subId != subs.get(i).getSubscriptionId()) {
364                 return false;
365             }
366         }
367         return true;
368     }
369 
370     @Override
setNoSims(boolean show, boolean simDetected)371     public void setNoSims(boolean show, boolean simDetected) {
372         // Noop yay!
373     }
374 
375     @Override
setEthernetIndicators(IconState state)376     public void setEthernetIndicators(IconState state) {
377         boolean visible = state.visible && !mHideEthernet;
378         int resId = state.icon;
379         String description = state.contentDescription;
380 
381         if (resId > 0) {
382             mIconController.setIcon(mSlotEthernet, resId, description);
383             mIconController.setIconVisibility(mSlotEthernet, true);
384         } else {
385             mIconController.setIconVisibility(mSlotEthernet, false);
386         }
387     }
388 
389     @Override
setIsAirplaneMode(IconState icon)390     public void setIsAirplaneMode(IconState icon) {
391         if (DEBUG) {
392             Log.d(TAG, "setIsAirplaneMode: "
393                     + "icon = " + (icon == null ? "" : icon.toString()));
394         }
395         mIsAirplaneMode = icon.visible && !mHideAirplane;
396         int resId = icon.icon;
397         String description = icon.contentDescription;
398 
399         if (mIsAirplaneMode && resId > 0) {
400             mIconController.setIcon(mSlotAirplane, resId, description);
401             mIconController.setIconVisibility(mSlotAirplane, true);
402         } else {
403             mIconController.setIconVisibility(mSlotAirplane, false);
404         }
405     }
406 
407     @Override
setMobileDataEnabled(boolean enabled)408     public void setMobileDataEnabled(boolean enabled) {
409         // Don't care.
410     }
411 
412     /**
413      * Stores the statusbar state for no Calling & SMS.
414      */
415     public static class CallIndicatorIconState {
416         public boolean isNoCalling;
417         public int noCallingResId;
418         public int callStrengthResId;
419         public int subId;
420         public String noCallingDescription;
421         public String callStrengthDescription;
422 
CallIndicatorIconState(int subId)423         private CallIndicatorIconState(int subId) {
424             this.subId = subId;
425             this.noCallingResId = R.drawable.ic_qs_no_calling_sms;
426             this.callStrengthResId = TelephonyIcons.MOBILE_CALL_STRENGTH_ICONS[0];
427         }
428 
429         @Override
equals(Object o)430         public boolean equals(Object o) {
431             // Skipping reference equality bc this should be more of a value type
432             if (o == null || getClass() != o.getClass()) {
433                 return false;
434             }
435             CallIndicatorIconState that = (CallIndicatorIconState) o;
436             return  isNoCalling == that.isNoCalling
437                     && noCallingResId == that.noCallingResId
438                     && callStrengthResId == that.callStrengthResId
439                     && subId == that.subId
440                     && noCallingDescription == that.noCallingDescription
441                     && callStrengthDescription == that.callStrengthDescription;
442 
443         }
444 
445         @Override
hashCode()446         public int hashCode() {
447             return Objects.hash(isNoCalling, noCallingResId,
448                     callStrengthResId, subId, noCallingDescription, callStrengthDescription);
449         }
450 
copyTo(CallIndicatorIconState other)451         private void copyTo(CallIndicatorIconState other) {
452             other.isNoCalling = isNoCalling;
453             other.noCallingResId = noCallingResId;
454             other.callStrengthResId = callStrengthResId;
455             other.subId = subId;
456             other.noCallingDescription = noCallingDescription;
457             other.callStrengthDescription = callStrengthDescription;
458         }
459 
copyStates( List<CallIndicatorIconState> inStates)460         private static List<CallIndicatorIconState> copyStates(
461                 List<CallIndicatorIconState> inStates) {
462             ArrayList<CallIndicatorIconState> outStates = new ArrayList<>();
463             for (CallIndicatorIconState state : inStates) {
464                 CallIndicatorIconState copy = new CallIndicatorIconState(state.subId);
465                 state.copyTo(copy);
466                 outStates.add(copy);
467             }
468             return outStates;
469         }
470     }
471 
472     private static abstract class SignalIconState {
473         public boolean visible;
474         public boolean activityOut;
475         public boolean activityIn;
476         public String slot;
477         public String contentDescription;
478 
479         @Override
equals(Object o)480         public boolean equals(Object o) {
481             // Skipping reference equality bc this should be more of a value type
482             if (o == null || getClass() != o.getClass()) {
483                 return false;
484             }
485             SignalIconState that = (SignalIconState) o;
486             return visible == that.visible &&
487                     activityOut == that.activityOut &&
488                     activityIn == that.activityIn &&
489                     Objects.equals(contentDescription, that.contentDescription) &&
490                     Objects.equals(slot, that.slot);
491         }
492 
493         @Override
hashCode()494         public int hashCode() {
495             return Objects.hash(visible, activityOut, slot);
496         }
497 
copyTo(SignalIconState other)498         protected void copyTo(SignalIconState other) {
499             other.visible = visible;
500             other.activityIn = activityIn;
501             other.activityOut = activityOut;
502             other.slot = slot;
503             other.contentDescription = contentDescription;
504         }
505     }
506 
507     public static class WifiIconState extends SignalIconState{
508         public int resId;
509         public boolean airplaneSpacerVisible;
510         public boolean signalSpacerVisible;
511         public boolean noDefaultNetwork;
512         public boolean noValidatedNetwork;
513         public boolean noNetworksAvailable;
514 
515         @Override
equals(Object o)516         public boolean equals(Object o) {
517             // Skipping reference equality bc this should be more of a value type
518             if (o == null || getClass() != o.getClass()) {
519                 return false;
520             }
521             if (!super.equals(o)) {
522                 return false;
523             }
524             WifiIconState that = (WifiIconState) o;
525             return resId == that.resId
526                     && airplaneSpacerVisible == that.airplaneSpacerVisible
527                     && signalSpacerVisible == that.signalSpacerVisible
528                     && noDefaultNetwork == that.noDefaultNetwork
529                     && noValidatedNetwork == that.noValidatedNetwork
530                     && noNetworksAvailable == that.noNetworksAvailable;
531         }
532 
copyTo(WifiIconState other)533         public void copyTo(WifiIconState other) {
534             super.copyTo(other);
535             other.resId = resId;
536             other.airplaneSpacerVisible = airplaneSpacerVisible;
537             other.signalSpacerVisible = signalSpacerVisible;
538             other.noDefaultNetwork = noDefaultNetwork;
539             other.noValidatedNetwork = noValidatedNetwork;
540             other.noNetworksAvailable = noNetworksAvailable;
541         }
542 
copy()543         public WifiIconState copy() {
544             WifiIconState newState = new WifiIconState();
545             copyTo(newState);
546             return newState;
547         }
548 
549         @Override
hashCode()550         public int hashCode() {
551             return Objects.hash(super.hashCode(),
552                     resId, airplaneSpacerVisible, signalSpacerVisible, noDefaultNetwork,
553                     noValidatedNetwork, noNetworksAvailable);
554         }
555 
toString()556         @Override public String toString() {
557             return "WifiIconState(resId=" + resId + ", visible=" + visible + ")";
558         }
559     }
560 
561     /**
562      * A little different. This one delegates to SignalDrawable instead of a specific resId
563      */
564     public static class MobileIconState extends SignalIconState {
565         public int subId;
566         public int strengthId;
567         public int typeId;
568         public boolean showTriangle;
569         public boolean roaming;
570         public boolean needsLeadingPadding;
571         public CharSequence typeContentDescription;
572 
MobileIconState(int subId)573         private MobileIconState(int subId) {
574             super();
575             this.subId = subId;
576         }
577 
578         @Override
equals(Object o)579         public boolean equals(Object o) {
580             if (o == null || getClass() != o.getClass()) {
581                 return false;
582             }
583             if (!super.equals(o)) {
584                 return false;
585             }
586             MobileIconState that = (MobileIconState) o;
587             return subId == that.subId
588                     && strengthId == that.strengthId
589                     && typeId == that.typeId
590                     && showTriangle == that.showTriangle
591                     && roaming == that.roaming
592                     && needsLeadingPadding == that.needsLeadingPadding
593                     && Objects.equals(typeContentDescription, that.typeContentDescription);
594         }
595 
596         @Override
hashCode()597         public int hashCode() {
598 
599             return Objects
600                     .hash(super.hashCode(), subId, strengthId, typeId, showTriangle, roaming,
601                             needsLeadingPadding, typeContentDescription);
602         }
603 
copy()604         public MobileIconState copy() {
605             MobileIconState copy = new MobileIconState(this.subId);
606             copyTo(copy);
607             return copy;
608         }
609 
copyTo(MobileIconState other)610         public void copyTo(MobileIconState other) {
611             super.copyTo(other);
612             other.subId = subId;
613             other.strengthId = strengthId;
614             other.typeId = typeId;
615             other.showTriangle = showTriangle;
616             other.roaming = roaming;
617             other.needsLeadingPadding = needsLeadingPadding;
618             other.typeContentDescription = typeContentDescription;
619         }
620 
copyStates(List<MobileIconState> inStates)621         private static List<MobileIconState> copyStates(List<MobileIconState> inStates) {
622             ArrayList<MobileIconState> outStates = new ArrayList<>();
623             for (MobileIconState state : inStates) {
624                 MobileIconState copy = new MobileIconState(state.subId);
625                 state.copyTo(copy);
626                 outStates.add(copy);
627             }
628 
629             return outStates;
630         }
631 
toString()632         @Override public String toString() {
633             return "MobileIconState(subId=" + subId + ", strengthId=" + strengthId
634                     + ", showTriangle=" + showTriangle + ", roaming=" + roaming
635                     + ", typeId=" + typeId + ", visible=" + visible + ")";
636         }
637     }
638 }
639