1 /*
2  * Copyright (C) 2019 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.internal.telephony.cdnr;
18 
19 import static com.android.internal.telephony.cdnr.EfData.EF_SOURCE_CARRIER_API;
20 import static com.android.internal.telephony.cdnr.EfData.EF_SOURCE_CARRIER_CONFIG;
21 import static com.android.internal.telephony.cdnr.EfData.EF_SOURCE_CSIM;
22 import static com.android.internal.telephony.cdnr.EfData.EF_SOURCE_DATA_OPERATOR_SIGNALLING;
23 import static com.android.internal.telephony.cdnr.EfData.EF_SOURCE_ERI;
24 import static com.android.internal.telephony.cdnr.EfData.EF_SOURCE_MODEM_CONFIG;
25 import static com.android.internal.telephony.cdnr.EfData.EF_SOURCE_RUIM;
26 import static com.android.internal.telephony.cdnr.EfData.EF_SOURCE_SIM;
27 import static com.android.internal.telephony.cdnr.EfData.EF_SOURCE_USIM;
28 import static com.android.internal.telephony.cdnr.EfData.EF_SOURCE_VOICE_OPERATOR_SIGNALLING;
29 
30 import android.annotation.NonNull;
31 import android.content.Context;
32 import android.content.res.Resources;
33 import android.os.PersistableBundle;
34 import android.telephony.CarrierConfigManager;
35 import android.telephony.Rlog;
36 import android.telephony.ServiceState;
37 import android.text.TextUtils;
38 import android.util.LocalLog;
39 import android.util.SparseArray;
40 
41 import com.android.internal.telephony.GsmCdmaPhone;
42 import com.android.internal.telephony.cdnr.EfData.EFSource;
43 import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
44 import com.android.internal.telephony.uicc.IccRecords;
45 import com.android.internal.telephony.uicc.IccRecords.CarrierNameDisplayConditionBitmask;
46 import com.android.internal.telephony.uicc.IccRecords.OperatorPlmnInfo;
47 import com.android.internal.telephony.uicc.IccRecords.PlmnNetworkName;
48 import com.android.internal.telephony.uicc.RuimRecords;
49 import com.android.internal.telephony.uicc.SIMRecords;
50 import com.android.internal.util.IndentingPrintWriter;
51 
52 import java.util.Arrays;
53 import java.util.Collections;
54 import java.util.List;
55 import java.util.Locale;
56 import java.util.Objects;
57 
58 /** Carrier display name resolver. */
59 public class CarrierDisplayNameResolver {
60     private static final boolean DBG = true;
61     private static final String TAG = "CDNR";
62 
63     /**
64      * Only display SPN in home network, and PLMN network name in roaming network.
65      */
66     @CarrierNameDisplayConditionBitmask
67     private static final int DEFAULT_CARRIER_NAME_DISPLAY_CONDITION_BITMASK = 0;
68 
69     private static final CarrierDisplayNameConditionRule DEFAULT_CARRIER_DISPLAY_NAME_RULE =
70             new CarrierDisplayNameConditionRule(DEFAULT_CARRIER_NAME_DISPLAY_CONDITION_BITMASK);
71 
72     private final SparseArray<EfData> mEf = new SparseArray<>();
73 
74     private final LocalLog mLocalLog;
75     private final Context mContext;
76     private final GsmCdmaPhone mPhone;
77     private final CarrierConfigManager mCCManager;
78 
79     private CarrierDisplayNameData mCarrierDisplayNameData;
80 
81     /**
82      * The priority of ef source. Lower index means higher priority.
83      */
84     private static final List<Integer> EF_SOURCE_PRIORITY =
85             Arrays.asList(
86                     EF_SOURCE_CARRIER_API,
87                     EF_SOURCE_CARRIER_CONFIG,
88                     EF_SOURCE_ERI,
89                     EF_SOURCE_USIM,
90                     EF_SOURCE_SIM,
91                     EF_SOURCE_CSIM,
92                     EF_SOURCE_RUIM,
93                     EF_SOURCE_VOICE_OPERATOR_SIGNALLING,
94                     EF_SOURCE_DATA_OPERATOR_SIGNALLING,
95                     EF_SOURCE_MODEM_CONFIG);
96 
CarrierDisplayNameResolver(GsmCdmaPhone phone)97     public CarrierDisplayNameResolver(GsmCdmaPhone phone) {
98         mLocalLog = new LocalLog(32);
99         mContext = phone.getContext();
100         mPhone = phone;
101         mCCManager = (CarrierConfigManager) mContext.getSystemService(
102                 Context.CARRIER_CONFIG_SERVICE);
103     }
104 
105     /**
106      * Update the ef from Ruim. If {@code ruim} is null, the ef records from this source will be
107      * removed.
108      *
109      * @param ruim Ruim records.
110      */
updateEfFromRuim(RuimRecords ruim)111     public void updateEfFromRuim(RuimRecords ruim) {
112         int key = getSourcePriority(EF_SOURCE_RUIM);
113         if (ruim == null) {
114             mEf.remove(key);
115         } else {
116             mEf.put(key, new RuimEfData(ruim));
117         }
118     }
119 
120     /**
121      * Update the ef from Usim. If {@code usim} is null, the ef records from this source will be
122      * removed.
123      *
124      * @param usim Usim records.
125      */
updateEfFromUsim(SIMRecords usim)126     public void updateEfFromUsim(SIMRecords usim) {
127         int key = getSourcePriority(EF_SOURCE_USIM);
128         if (usim == null) {
129             mEf.remove(key);
130         } else {
131             mEf.put(key, new UsimEfData(usim));
132         }
133     }
134 
135     /**
136      * Update the ef from carrier config. If {@code config} is null, the ef records from this source
137      * will be removed.
138      *
139      * @param config carrier config.
140      */
updateEfFromCarrierConfig(PersistableBundle config)141     public void updateEfFromCarrierConfig(PersistableBundle config) {
142         int key = getSourcePriority(EF_SOURCE_CARRIER_CONFIG);
143         if (config == null) {
144             mEf.remove(key);
145         } else {
146             mEf.put(key, new CarrierConfigEfData(config));
147         }
148     }
149 
150     /**
151      * Update the ef for CDMA eri text. The ef records from this source will be set all of the
152      * following situation are satisfied.
153      *
154      * 1. {@code eriText} is neither empty nor null.
155      * 2. Current network is CDMA or CdmaLte
156      * 3. ERI is allowed.
157      *
158      * @param eriText
159      */
updateEfForEri(String eriText)160     public void updateEfForEri(String eriText) {
161         PersistableBundle config = getCarrierConfig();
162         int key = getSourcePriority(EF_SOURCE_ERI);
163         if (!TextUtils.isEmpty(eriText) && (mPhone.isPhoneTypeCdma() || mPhone.isPhoneTypeCdmaLte())
164                 && config.getBoolean(CarrierConfigManager.KEY_ALLOW_ERI_BOOL)) {
165             mEf.put(key, new EriEfData(eriText));
166         } else {
167             mEf.remove(key);
168         }
169     }
170 
171     /**
172      * Update the ef for brandOverride. If {@code operatorName} is empty or null, the ef records
173      * from this source will be removed.
174      *
175      * @param operatorName operator name from brand override.
176      */
updateEfForBrandOverride(String operatorName)177     public void updateEfForBrandOverride(String operatorName) {
178         int key = getSourcePriority(EF_SOURCE_CARRIER_API);
179         if (TextUtils.isEmpty(operatorName)) {
180             mEf.remove(key);
181         } else {
182             mEf.put(key,
183                     new BrandOverrideEfData(operatorName, getServiceState().getOperatorNumeric()));
184         }
185     }
186 
187     /** Get the resolved carrier display name. */
getCarrierDisplayNameData()188     public CarrierDisplayNameData getCarrierDisplayNameData() {
189         resolveCarrierDisplayName();
190         return mCarrierDisplayNameData;
191     }
192 
193     @Override
toString()194     public String toString() {
195         StringBuilder sb = new StringBuilder();
196         for (int i = 0; i < mEf.size(); i++) {
197             EfData p = mEf.valueAt(i);
198             sb.append("{spnDisplayCondition = " + p.getServiceProviderNameDisplayCondition()
199                     + ", spn = " + p.getServiceProviderName()
200                     + ", spdiList = " + p.getServiceProviderDisplayInformation()
201                     + ", pnnList = " + p.getPlmnNetworkNameList()
202                     + ", oplList = " + p.getOperatorPlmnList()
203                     + ", ehplmn = " + p.getEhplmnList()
204                     + "}, ");
205         }
206         sb.append(", roamingFromSS = " + getServiceState().getRoaming());
207         sb.append(", registeredPLMN = " + getServiceState().getOperatorNumeric());
208         return sb.toString();
209     }
210 
211     /**
212      * Dumps information for carrier display name resolver.
213      * @param pw information printer.
214      */
dump(IndentingPrintWriter pw)215     public void dump(IndentingPrintWriter pw) {
216         pw.println("CDNR:");
217         pw.increaseIndent();
218         pw.println("fields = " + toString());
219         pw.println("carrierDisplayNameData = " + mCarrierDisplayNameData);
220         pw.decreaseIndent();
221 
222         pw.println("CDNR local log:");
223         pw.increaseIndent();
224         mLocalLog.dump(pw);
225         pw.decreaseIndent();
226     }
227 
228     @NonNull
getCarrierConfig()229     private PersistableBundle getCarrierConfig() {
230         PersistableBundle config = mCCManager.getConfigForSubId(mPhone.getSubId());
231         if (config == null) config = CarrierConfigManager.getDefaultConfig();
232         return config;
233     }
234 
235     @NonNull
getDisplayRule()236     private CarrierDisplayNameConditionRule getDisplayRule() {
237         for (int i = 0; i < mEf.size(); i++) {
238             if (mEf.valueAt(i).getServiceProviderNameDisplayCondition()
239                     != IccRecords.INVALID_CARRIER_NAME_DISPLAY_CONDITION_BITMASK) {
240                 return new CarrierDisplayNameConditionRule(
241                         mEf.valueAt(i).getServiceProviderNameDisplayCondition());
242             }
243         }
244         return DEFAULT_CARRIER_DISPLAY_NAME_RULE;
245     }
246 
247     @NonNull
getEfSpdi()248     private List<String> getEfSpdi() {
249         for (int i = 0; i < mEf.size(); i++) {
250             if (mEf.valueAt(i).getServiceProviderDisplayInformation() != null) {
251                 return mEf.valueAt(i).getServiceProviderDisplayInformation();
252             }
253         }
254         return Collections.EMPTY_LIST;
255     }
256 
257     @NonNull
getEfSpn()258     private String getEfSpn() {
259         for (int i = 0; i < mEf.size(); i++) {
260             if (!TextUtils.isEmpty(mEf.valueAt(i).getServiceProviderName())) {
261                 return mEf.valueAt(i).getServiceProviderName();
262             }
263         }
264         return "";
265     }
266 
267     @NonNull
getEfOpl()268     private List<OperatorPlmnInfo> getEfOpl() {
269         for (int i = 0; i < mEf.size(); i++) {
270             if (mEf.valueAt(i).getOperatorPlmnList() != null) {
271                 return mEf.valueAt(i).getOperatorPlmnList();
272             }
273         }
274         return Collections.EMPTY_LIST;
275     }
276 
277     @NonNull
getEfPnn()278     private List<PlmnNetworkName> getEfPnn() {
279         for (int i = 0; i < mEf.size(); i++) {
280             if (mEf.valueAt(i).getPlmnNetworkNameList() != null) {
281                 return mEf.valueAt(i).getPlmnNetworkNameList();
282             }
283         }
284         return Collections.EMPTY_LIST;
285     }
286 
getCarrierDisplayNameFromEf()287     private CarrierDisplayNameData getCarrierDisplayNameFromEf() {
288         CarrierDisplayNameConditionRule displayRule = getDisplayRule();
289 
290         String registeredPlmnNumeric = getServiceState().getOperatorNumeric();
291         List<String> efSpdi = getEfSpdi();
292 
293         // Currently use the roaming state from ServiceState.
294         // EF_SPDI is only used when determine the service provider name and PLMN network name
295         // display condition rule.
296         // All the PLMNs will be considered HOME PLMNs if there is a brand override.
297         boolean isRoaming = getServiceState().getRoaming()
298                 && !efSpdi.contains(registeredPlmnNumeric);
299         boolean showSpn = displayRule.shouldShowSpn(isRoaming);
300         boolean showPlmn = displayRule.shouldShowPnn(isRoaming);
301         String spn = getEfSpn();
302 
303         // Resolve the PLMN network name
304         List<OperatorPlmnInfo> efOpl = getEfOpl();
305         List<PlmnNetworkName> efPnn = getEfPnn();
306 
307         String plmn = null;
308         if (efOpl.isEmpty()) {
309             // If the EF_OPL is not present, then the first record in EF_PNN is used for the
310             // default network name when registered in the HPLMN or an EHPLMN(if the EHPLMN list
311             // is present).
312             plmn = efPnn.isEmpty() ? "" : getPlmnNetworkName(efPnn.get(0));
313         } else {
314             // TODO: Check the TAC/LAC & registered PLMN numeric in OPL list to determine which
315             // PLMN name should be used to override the current one.
316         }
317 
318         // If no PLMN override is present, then the PLMN should be displayed numerically.
319         if (TextUtils.isEmpty(plmn)) {
320             plmn = registeredPlmnNumeric;
321         }
322 
323         return new CarrierDisplayNameData.Builder()
324                 .setSpn(spn)
325                 .setShowSpn(showSpn)
326                 .setPlmn(plmn)
327                 .setShowPlmn(showPlmn)
328                 .build();
329     }
330 
getCarrierDisplayNameFromWifiCallingOverride( CarrierDisplayNameData rawCarrierDisplayNameData)331     private CarrierDisplayNameData getCarrierDisplayNameFromWifiCallingOverride(
332             CarrierDisplayNameData rawCarrierDisplayNameData) {
333         PersistableBundle config = getCarrierConfig();
334         boolean useRootLocale = config.getBoolean(CarrierConfigManager.KEY_WFC_SPN_USE_ROOT_LOCALE);
335         Resources r = mContext.getResources();
336         if (useRootLocale) r.getConfiguration().setLocale(Locale.ROOT);
337         String[] wfcSpnFormats = r.getStringArray(com.android.internal.R.array.wfcSpnFormats);
338         WfcCarrierNameFormatter wfcFormatter = new WfcCarrierNameFormatter(config, wfcSpnFormats,
339                 getServiceState().getVoiceRegState() == ServiceState.STATE_POWER_OFF);
340 
341         // Override the spn, data spn, plmn by wifi-calling
342         String wfcSpn = wfcFormatter.formatVoiceName(rawCarrierDisplayNameData.getSpn());
343         String wfcDataSpn = wfcFormatter.formatDataName(rawCarrierDisplayNameData.getSpn());
344         String wfcPlmn = wfcFormatter.formatVoiceName(rawCarrierDisplayNameData.getPlmn());
345         CarrierDisplayNameData result = rawCarrierDisplayNameData;
346         if (!TextUtils.isEmpty(wfcSpn) && !TextUtils.isEmpty(wfcDataSpn)) {
347             result = new CarrierDisplayNameData.Builder()
348                     .setSpn(wfcSpn)
349                     .setDataSpn(wfcDataSpn)
350                     .setShowSpn(true)
351                     .build();
352         } else if (!TextUtils.isEmpty(wfcPlmn)) {
353             result = new CarrierDisplayNameData.Builder()
354                     .setPlmn(wfcPlmn)
355                     .setShowPlmn(true)
356                     .build();
357         }
358         return result;
359     }
360 
361     /**
362      * Override the given carrier display name data {@code data} by out of service rule.
363      * @param data the carrier display name data need to be overridden.
364      * @return overridden carrier display name data.
365      */
getOutOfServiceDisplayName(CarrierDisplayNameData data)366     private CarrierDisplayNameData getOutOfServiceDisplayName(CarrierDisplayNameData data) {
367         // Out of service/Power off/Emergency Only override
368         // 1) In flight mode(service state is ServiceState.STATE_POWER_OFF), or the service
369         //    state is ServiceState.STATE_OUT_OF_SERVICE but emergency call is not allowed.
370         //    showPlmn = true
371         //    Only show "No Service" as PLMN
372         //
373         // 2) Out of service but emergency call is allowed.
374         //    showPlmn = true
375         //    Only show "Emergency call only" as PLMN
376         String plmn = null;
377         boolean isSimReady = mPhone.getUiccCardApplication() != null
378                 && mPhone.getUiccCardApplication().getState() == AppState.APPSTATE_READY;
379         boolean forceDisplayNoService = mContext.getResources().getBoolean(
380                 com.android.internal.R.bool.config_display_no_service_when_sim_unready)
381                 && !isSimReady;
382         ServiceState ss = getServiceState();
383         if (ss.getVoiceRegState() == ServiceState.STATE_POWER_OFF || !ss.isEmergencyOnly()
384                 || forceDisplayNoService) {
385             plmn = mContext.getResources().getString(
386                     com.android.internal.R.string.lockscreen_carrier_default);
387         } else {
388             plmn = mContext.getResources().getString(
389                     com.android.internal.R.string.emergency_calls_only);
390         }
391         return new CarrierDisplayNameData.Builder()
392                 .setSpn(data.getSpn())
393                 .setDataSpn(data.getDataSpn())
394                 .setShowSpn(data.shouldShowSpn())
395                 .setPlmn(plmn)
396                 .setShowPlmn(true)
397                 .build();
398     }
399 
resolveCarrierDisplayName()400     private void resolveCarrierDisplayName() {
401         CarrierDisplayNameData data = getCarrierDisplayNameFromEf();
402         if (DBG) Rlog.d(TAG, "CarrierName from EF: " + data);
403         if (getCombinedRegState(getServiceState()) == ServiceState.STATE_IN_SERVICE) {
404             if (mPhone.isWifiCallingEnabled()) {
405                 data = getCarrierDisplayNameFromWifiCallingOverride(data);
406                 if (DBG) {
407                     Rlog.d(TAG, "CarrierName override by wifi-calling " + data);
408                 }
409             }
410         } else {
411             data = getOutOfServiceDisplayName(data);
412             if (DBG) Rlog.d(TAG, "Out of service carrierName " + data);
413         }
414 
415         if (!Objects.equals(mCarrierDisplayNameData, data)) {
416             mLocalLog.log(String.format("ResolveCarrierDisplayName: %s", data.toString()));
417         }
418 
419         mCarrierDisplayNameData = data;
420     }
421 
422     /**
423      * Get the PLMN network name from the {@link PlmnNetworkName} object.
424      * @param name the {@link PlmnNetworkName} object may contain the full and short version of PLMN
425      * network name.
426      * @return full/short version PLMN network name if one of those is existed, otherwise return an
427      * empty string.
428      */
getPlmnNetworkName(PlmnNetworkName name)429     private static String getPlmnNetworkName(PlmnNetworkName name) {
430         if (name == null) return "";
431         if (!TextUtils.isEmpty(name.fullName)) return name.fullName;
432         if (!TextUtils.isEmpty(name.shortName)) return name.shortName;
433         return "";
434     }
435 
436     /**
437      * Get the priority of the source of ef object. If {@code source} is not in the priority list,
438      * return {@link Integer#MAX_VALUE}.
439      * @param source source of ef object.
440      * @return the priority of the source of ef object.
441      */
getSourcePriority(@FSource int source)442     private static int getSourcePriority(@EFSource int source) {
443         int priority = EF_SOURCE_PRIORITY.indexOf(source);
444         if (priority == -1) priority = Integer.MAX_VALUE;
445         return priority;
446     }
447 
448     private static final class CarrierDisplayNameConditionRule {
449         private int mDisplayConditionBitmask;
450 
CarrierDisplayNameConditionRule(int carrierDisplayConditionBitmask)451         CarrierDisplayNameConditionRule(int carrierDisplayConditionBitmask) {
452             mDisplayConditionBitmask = carrierDisplayConditionBitmask;
453         }
454 
shouldShowSpn(boolean isRoaming)455         boolean shouldShowSpn(boolean isRoaming) {
456             return !isRoaming || ((mDisplayConditionBitmask
457                     & IccRecords.CARRIER_NAME_DISPLAY_CONDITION_BITMASK_SPN)
458                     == IccRecords.CARRIER_NAME_DISPLAY_CONDITION_BITMASK_SPN);
459         }
460 
shouldShowPnn(boolean isRoaming)461         boolean shouldShowPnn(boolean isRoaming) {
462             return isRoaming || ((mDisplayConditionBitmask
463                     & IccRecords.CARRIER_NAME_DISPLAY_CONDITION_BITMASK_PLMN)
464                     == IccRecords.CARRIER_NAME_DISPLAY_CONDITION_BITMASK_PLMN);
465         }
466 
467         @Override
toString()468         public String toString() {
469             return String.format("{ SPN_bit = %d, PLMN_bit = %d }",
470                     mDisplayConditionBitmask
471                             & IccRecords.CARRIER_NAME_DISPLAY_CONDITION_BITMASK_SPN,
472                     mDisplayConditionBitmask
473                             & IccRecords.CARRIER_NAME_DISPLAY_CONDITION_BITMASK_PLMN);
474         }
475     }
476 
getServiceState()477     private ServiceState getServiceState() {
478         return mPhone.getServiceStateTracker().getServiceState();
479     }
480 
481     /**
482      * WiFi-Calling formatter for carrier name.
483      */
484     private static final class WfcCarrierNameFormatter {
485         final String mVoiceFormat;
486         final String mDataFormat;
487 
WfcCarrierNameFormatter(@onNull PersistableBundle config, @NonNull String[] wfcFormats, boolean inFlightMode)488         WfcCarrierNameFormatter(@NonNull PersistableBundle config,
489                 @NonNull String[] wfcFormats, boolean inFlightMode) {
490             int voiceIdx = config.getInt(CarrierConfigManager.KEY_WFC_SPN_FORMAT_IDX_INT);
491             int dataIdx = config.getInt(CarrierConfigManager.KEY_WFC_DATA_SPN_FORMAT_IDX_INT);
492             int flightModeIdx = config.getInt(
493                     CarrierConfigManager.KEY_WFC_FLIGHT_MODE_SPN_FORMAT_IDX_INT);
494 
495             if (voiceIdx < 0 || voiceIdx >= wfcFormats.length) {
496                 Rlog.e(TAG, "updateSpnDisplay: KEY_WFC_SPN_FORMAT_IDX_INT out of bounds: "
497                         + voiceIdx);
498                 voiceIdx = 0;
499             }
500 
501             if (dataIdx < 0 || dataIdx >= wfcFormats.length) {
502                 Rlog.e(TAG, "updateSpnDisplay: KEY_WFC_DATA_SPN_FORMAT_IDX_INT out of bounds: "
503                         + dataIdx);
504                 dataIdx = 0;
505             }
506 
507             if (flightModeIdx < 0 || flightModeIdx >= wfcFormats.length) {
508                 // KEY_WFC_FLIGHT_MODE_SPN_FORMAT_IDX_INT out of bounds. Use the value from
509                 // voiceIdx.
510                 flightModeIdx = voiceIdx;
511             }
512 
513             // flight mode
514             if (inFlightMode) {
515                 voiceIdx = flightModeIdx;
516             }
517 
518             mVoiceFormat = voiceIdx != -1 ? wfcFormats[voiceIdx] : "";
519             mDataFormat = dataIdx != -1 ? wfcFormats[dataIdx] : "";
520         }
521 
522         /**
523          * Format the given {@code name} using wifi-calling voice name formatter.
524          * @param name the string need to be formatted.
525          * @return formatted string if {@code name} is not empty, otherwise return {@code name}.
526          */
formatVoiceName(String name)527         public String formatVoiceName(String name) {
528             if (TextUtils.isEmpty(name)) return name;
529             return String.format(mVoiceFormat, name.trim());
530         }
531 
532         /**
533          * Format the given {@code name} using wifi-calling data name formatter.
534          * @param name the string need to be formatted.
535          * @return formatted string if {@code name} is not empty, otherwise return {@code name}.
536          */
formatDataName(String name)537         public String formatDataName(String name) {
538             if (TextUtils.isEmpty(name)) return name;
539             return String.format(mDataFormat, name.trim());
540         }
541     }
542 
543     /**
544      * Consider dataRegState if voiceRegState is OOS to determine SPN to be displayed.
545      * @param ss service state.
546      */
getCombinedRegState(ServiceState ss)547     private static int getCombinedRegState(ServiceState ss) {
548         if (ss.getVoiceRegState() != ServiceState.STATE_IN_SERVICE) return ss.getDataRegState();
549         return ss.getVoiceRegState();
550     }
551 }
552