1 /*
2  * Copyright 2017 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 package com.android.internal.telephony;
17 
18 import static android.provider.Telephony.CarrierId;
19 
20 import android.annotation.NonNull;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.database.ContentObserver;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.Handler;
28 import android.os.Message;
29 import android.provider.Telephony;
30 import android.service.carrier.CarrierIdentifier;
31 import android.telephony.PhoneStateListener;
32 import android.telephony.Rlog;
33 import android.telephony.SubscriptionManager;
34 import android.telephony.TelephonyManager;
35 import android.text.TextUtils;
36 import android.util.LocalLog;
37 import android.util.Log;
38 
39 import com.android.internal.annotations.VisibleForTesting;
40 import com.android.internal.telephony.metrics.TelephonyMetrics;
41 import com.android.internal.telephony.uicc.IccRecords;
42 import com.android.internal.telephony.uicc.UiccController;
43 import com.android.internal.util.IndentingPrintWriter;
44 
45 import java.io.FileDescriptor;
46 import java.io.PrintWriter;
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.List;
50 
51 /**
52  * CarrierResolver identifies the subscription carrier and returns a canonical carrier Id
53  * and a user friendly carrier name. CarrierResolver reads subscription info and check against
54  * all carrier matching rules stored in CarrierIdProvider. It is msim aware, each phone has a
55  * dedicated CarrierResolver.
56  */
57 public class CarrierResolver extends Handler {
58     private static final String LOG_TAG = CarrierResolver.class.getSimpleName();
59     private static final boolean DBG = true;
60     private static final boolean VDBG = Rlog.isLoggable(LOG_TAG, Log.VERBOSE);
61 
62     // events to trigger carrier identification
63     private static final int SIM_LOAD_EVENT             = 1;
64     private static final int ICC_CHANGED_EVENT          = 2;
65     private static final int PREFER_APN_UPDATE_EVENT    = 3;
66     private static final int CARRIER_ID_DB_UPDATE_EVENT = 4;
67 
68     private static final Uri CONTENT_URL_PREFER_APN = Uri.withAppendedPath(
69             Telephony.Carriers.CONTENT_URI, "preferapn");
70 
71     // cached matching rules based mccmnc to speed up resolution
72     private List<CarrierMatchingRule> mCarrierMatchingRulesOnMccMnc = new ArrayList<>();
73     // cached carrier Id
74     private int mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
75     // cached specific carrier Id
76     private int mSpecificCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
77     // cached MNO carrier Id. mno carrier shares the same mccmnc as cid and can be solely
78     // identified by mccmnc only. If there is no such mno carrier, mno carrier id equals to
79     // the cid.
80     private int mMnoCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
81     // cached carrier name
82     private String mCarrierName;
83     private String mSpecificCarrierName;
84     // cached preferapn name
85     private String mPreferApn;
86     // override for testing purpose
87     private String mTestOverrideApn;
88     private String mTestOverrideCarrierPriviledgeRule;
89     // cached service provider name. telephonyManager API returns empty string as default value.
90     // some carriers need to target devices with Empty SPN. In that case, carrier matching rule
91     // should specify "" spn explicitly.
92     private String mSpn = "";
93 
94     private Context mContext;
95     private Phone mPhone;
96     private IccRecords mIccRecords;
97     private final LocalLog mCarrierIdLocalLog = new LocalLog(20);
98     private final TelephonyManager mTelephonyMgr;
99 
100     private final ContentObserver mContentObserver = new ContentObserver(this) {
101         @Override
102         public void onChange(boolean selfChange, Uri uri) {
103             if (CONTENT_URL_PREFER_APN.equals(uri.getLastPathSegment())) {
104                 logd("onChange URI: " + uri);
105                 sendEmptyMessage(PREFER_APN_UPDATE_EVENT);
106             } else if (CarrierId.All.CONTENT_URI.equals(uri)) {
107                 logd("onChange URI: " + uri);
108                 sendEmptyMessage(CARRIER_ID_DB_UPDATE_EVENT);
109             }
110         }
111     };
112 
CarrierResolver(Phone phone)113     public CarrierResolver(Phone phone) {
114         logd("Creating CarrierResolver[" + phone.getPhoneId() + "]");
115         mContext = phone.getContext();
116         mPhone = phone;
117         mTelephonyMgr = TelephonyManager.from(mContext);
118 
119         // register events
120         mContext.getContentResolver().registerContentObserver(CONTENT_URL_PREFER_APN, false,
121                 mContentObserver);
122         mContext.getContentResolver().registerContentObserver(
123                 CarrierId.All.CONTENT_URI, false, mContentObserver);
124         UiccController.getInstance().registerForIccChanged(this, ICC_CHANGED_EVENT, null);
125     }
126 
127     /**
128      * This is triggered from SubscriptionInfoUpdater after sim state change.
129      * The sequence of sim loading would be
130      *  1. ACTION_SUBINFO_CONTENT_CHANGE
131      *  2. ACTION_SIM_STATE_CHANGED/ACTION_SIM_CARD_STATE_CHANGED
132      *  /ACTION_SIM_APPLICATION_STATE_CHANGED
133      *  3. ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED
134      *
135      *  For SIM refresh either reset or init refresh type, SubscriptionInfoUpdater will re-trigger
136      *  carrier identification with sim loaded state. Framework today silently handle single file
137      *  refresh type.
138      *  TODO: check fileId from single file refresh, if the refresh file is IMSI, gid1 or other
139      *  records which might change carrier id, framework should trigger sim loaded state just like
140      *  other refresh events: INIT or RESET and which will ultimately trigger carrier
141      *  re-identification.
142      */
resolveSubscriptionCarrierId(String simState)143     public void resolveSubscriptionCarrierId(String simState) {
144         logd("[resolveSubscriptionCarrierId] simState: " + simState);
145         switch (simState) {
146             case IccCardConstants.INTENT_VALUE_ICC_ABSENT:
147             case IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR:
148                 // only clear carrier id on absent to avoid transition to unknown carrier id during
149                 // intermediate states of sim refresh
150                 handleSimAbsent();
151                 break;
152             case IccCardConstants.INTENT_VALUE_ICC_LOADED:
153                 handleSimLoaded();
154                 break;
155         }
156     }
157 
handleSimLoaded()158     private void handleSimLoaded() {
159         if (mIccRecords != null) {
160             /**
161              * returns empty string to be consistent with
162              * {@link TelephonyManager#getSimOperatorName()}
163              */
164             mSpn = (mIccRecords.getServiceProviderName() == null) ? ""
165                     : mIccRecords.getServiceProviderName();
166         } else {
167             loge("mIccRecords is null on SIM_LOAD_EVENT, could not get SPN");
168         }
169         mPreferApn = getPreferApn();
170         loadCarrierMatchingRulesOnMccMnc();
171     }
172 
handleSimAbsent()173     private void handleSimAbsent() {
174         mCarrierMatchingRulesOnMccMnc.clear();
175         mSpn = null;
176         mPreferApn = null;
177         updateCarrierIdAndName(TelephonyManager.UNKNOWN_CARRIER_ID, null,
178                 TelephonyManager.UNKNOWN_CARRIER_ID, null,
179                 TelephonyManager.UNKNOWN_CARRIER_ID);
180     }
181 
182     private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
183         @Override
184         public void onCallStateChanged(int state, String ignored) {
185         }
186     };
187 
188     /**
189      * Entry point for the carrier identification.
190      *
191      *    1. SIM_LOAD_EVENT
192      *        This indicates that all SIM records has been loaded and its first entry point for the
193      *        carrier identification. Note, there are other attributes could be changed on the fly
194      *        like APN. We cached all carrier matching rules based on MCCMNC to speed
195      *        up carrier resolution on following trigger events.
196      *
197      *    2. PREFER_APN_UPDATE_EVENT
198      *        This indicates prefer apn has been changed. It could be triggered when user modified
199      *        APN settings or when default data connection first establishes on the current carrier.
200      *        We follow up on this by querying prefer apn sqlite and re-issue carrier identification
201      *        with the updated prefer apn name.
202      *
203      *    3. CARRIER_ID_DB_UPDATE_EVENT
204      *        This indicates that carrierIdentification database which stores all matching rules
205      *        has been updated. It could be triggered from OTA or assets update.
206      */
207     @Override
handleMessage(Message msg)208     public void handleMessage(Message msg) {
209         if (DBG) logd("handleMessage: " + msg.what);
210         switch (msg.what) {
211             case SIM_LOAD_EVENT:
212                 handleSimLoaded();
213                 break;
214             case CARRIER_ID_DB_UPDATE_EVENT:
215                 loadCarrierMatchingRulesOnMccMnc();
216                 break;
217             case PREFER_APN_UPDATE_EVENT:
218                 String preferApn = getPreferApn();
219                 if (!equals(mPreferApn, preferApn, true)) {
220                     logd("[updatePreferApn] from:" + mPreferApn + " to:" + preferApn);
221                     mPreferApn = preferApn;
222                     matchSubscriptionCarrier();
223                 }
224                 break;
225             case ICC_CHANGED_EVENT:
226                 // all records used for carrier identification are from SimRecord.
227                 final IccRecords newIccRecords = UiccController.getInstance().getIccRecords(
228                         mPhone.getPhoneId(), UiccController.APP_FAM_3GPP);
229                 if (mIccRecords != newIccRecords) {
230                     if (mIccRecords != null) {
231                         logd("Removing stale icc objects.");
232                         mIccRecords.unregisterForRecordsOverride(this);
233                         mIccRecords = null;
234                     }
235                     if (newIccRecords != null) {
236                         logd("new Icc object");
237                         newIccRecords.registerForRecordsOverride(this, SIM_LOAD_EVENT, null);
238                         mIccRecords = newIccRecords;
239                     }
240                 }
241                 break;
242             default:
243                 loge("invalid msg: " + msg.what);
244                 break;
245         }
246     }
247 
loadCarrierMatchingRulesOnMccMnc()248     private void loadCarrierMatchingRulesOnMccMnc() {
249         try {
250             String mccmnc = mTelephonyMgr.getSimOperatorNumericForPhone(mPhone.getPhoneId());
251             Cursor cursor = mContext.getContentResolver().query(
252                     CarrierId.All.CONTENT_URI,
253                     /* projection */ null,
254                     /* selection */ CarrierId.All.MCCMNC + "=?",
255                     /* selectionArgs */ new String[]{mccmnc}, null);
256             try {
257                 if (cursor != null) {
258                     if (VDBG) {
259                         logd("[loadCarrierMatchingRules]- " + cursor.getCount()
260                                 + " Records(s) in DB" + " mccmnc: " + mccmnc);
261                     }
262                     mCarrierMatchingRulesOnMccMnc.clear();
263                     while (cursor.moveToNext()) {
264                         mCarrierMatchingRulesOnMccMnc.add(makeCarrierMatchingRule(cursor));
265                     }
266                     matchSubscriptionCarrier();
267                 }
268             } finally {
269                 if (cursor != null) {
270                     cursor.close();
271                 }
272             }
273         } catch (Exception ex) {
274             loge("[loadCarrierMatchingRules]- ex: " + ex);
275         }
276     }
277 
getCarrierNameFromId(int cid)278     private String getCarrierNameFromId(int cid) {
279         try {
280             Cursor cursor = mContext.getContentResolver().query(
281                     CarrierId.All.CONTENT_URI,
282                     /* projection */ null,
283                     /* selection */ CarrierId.CARRIER_ID + "=?",
284                     /* selectionArgs */ new String[]{cid + ""}, null);
285             try {
286                 if (cursor != null) {
287                     if (VDBG) {
288                         logd("[getCarrierNameFromId]- " + cursor.getCount()
289                                 + " Records(s) in DB" + " cid: " + cid);
290                     }
291                     while (cursor.moveToNext()) {
292                         return cursor.getString(cursor.getColumnIndex(CarrierId.CARRIER_NAME));
293                     }
294                 }
295             } finally {
296                 if (cursor != null) {
297                     cursor.close();
298                 }
299             }
300         } catch (Exception ex) {
301             loge("[getCarrierNameFromId]- ex: " + ex);
302         }
303         return null;
304     }
305 
getCarrierMatchingRulesFromMccMnc( @onNull Context context, String mccmnc)306     private static List<CarrierMatchingRule> getCarrierMatchingRulesFromMccMnc(
307             @NonNull Context context, String mccmnc) {
308         List<CarrierMatchingRule> rules = new ArrayList<>();
309         try {
310             Cursor cursor = context.getContentResolver().query(
311                     CarrierId.All.CONTENT_URI,
312                     /* projection */ null,
313                     /* selection */ CarrierId.All.MCCMNC + "=?",
314                     /* selectionArgs */ new String[]{mccmnc}, null);
315             try {
316                 if (cursor != null) {
317                     if (VDBG) {
318                         logd("[loadCarrierMatchingRules]- " + cursor.getCount()
319                                 + " Records(s) in DB" + " mccmnc: " + mccmnc);
320                     }
321                     rules.clear();
322                     while (cursor.moveToNext()) {
323                         rules.add(makeCarrierMatchingRule(cursor));
324                     }
325                 }
326             } finally {
327                 if (cursor != null) {
328                     cursor.close();
329                 }
330             }
331         } catch (Exception ex) {
332             loge("[loadCarrierMatchingRules]- ex: " + ex);
333         }
334         return rules;
335     }
336 
getPreferApn()337     private String getPreferApn() {
338         // return test overrides if present
339         if (!TextUtils.isEmpty(mTestOverrideApn)) {
340             logd("[getPreferApn]- " + mTestOverrideApn + " test override");
341             return mTestOverrideApn;
342         }
343         Cursor cursor = mContext.getContentResolver().query(
344                 Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "preferapn/subId/"
345                 + mPhone.getSubId()), /* projection */ new String[]{Telephony.Carriers.APN},
346                 /* selection */ null, /* selectionArgs */ null, /* sortOrder */ null);
347         try {
348             if (cursor != null) {
349                 if (VDBG) {
350                     logd("[getPreferApn]- " + cursor.getCount() + " Records(s) in DB");
351                 }
352                 while (cursor.moveToNext()) {
353                     String apn = cursor.getString(cursor.getColumnIndexOrThrow(
354                             Telephony.Carriers.APN));
355                     logd("[getPreferApn]- " + apn);
356                     return apn;
357                 }
358             }
359         } catch (Exception ex) {
360             loge("[getPreferApn]- exception: " + ex);
361         } finally {
362             if (cursor != null) {
363                 cursor.close();
364             }
365         }
366         return null;
367     }
368 
isPreferApnUserEdited(@onNull String preferApn)369     private boolean isPreferApnUserEdited(@NonNull String preferApn) {
370         try (Cursor cursor = mContext.getContentResolver().query(
371                 Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI,
372                         "preferapn/subId/" + mPhone.getSubId()),
373                 /* projection */ new String[]{Telephony.Carriers.EDITED_STATUS},
374                 /* selection */ Telephony.Carriers.APN + "=?",
375                 /* selectionArgs */ new String[]{preferApn}, /* sortOrder */ null) ) {
376             if (cursor != null && cursor.moveToFirst()) {
377                 return cursor.getInt(cursor.getColumnIndexOrThrow(
378                         Telephony.Carriers.EDITED_STATUS)) == Telephony.Carriers.USER_EDITED;
379             }
380         } catch (Exception ex) {
381             loge("[isPreferApnUserEdited]- exception: " + ex);
382         }
383         return false;
384     }
385 
setTestOverrideApn(String apn)386     public void setTestOverrideApn(String apn) {
387         logd("[setTestOverrideApn]: " + apn);
388         mTestOverrideApn = apn;
389     }
390 
setTestOverrideCarrierPriviledgeRule(String rule)391     public void setTestOverrideCarrierPriviledgeRule(String rule) {
392         logd("[setTestOverrideCarrierPriviledgeRule]: " + rule);
393         mTestOverrideCarrierPriviledgeRule = rule;
394     }
395 
updateCarrierIdAndName(int cid, String name, int specificCarrierId, String specificCarrierName, int mnoCid)396     private void updateCarrierIdAndName(int cid, String name,
397                                         int specificCarrierId, String specificCarrierName,
398                                         int mnoCid) {
399         boolean update = false;
400         if (specificCarrierId != mSpecificCarrierId) {
401             logd("[updateSpecificCarrierId] from:" + mSpecificCarrierId + " to:"
402                     + specificCarrierId);
403             mSpecificCarrierId = specificCarrierId;
404             update = true;
405         }
406         if (specificCarrierName != mSpecificCarrierName) {
407             logd("[updateSpecificCarrierName] from:" + mSpecificCarrierName + " to:"
408                     + specificCarrierName);
409             mSpecificCarrierName = specificCarrierName;
410             update = true;
411         }
412         if (update) {
413             mCarrierIdLocalLog.log("[updateSpecificCarrierIdAndName] cid:"
414                     + mSpecificCarrierId + " name:" + mSpecificCarrierName);
415             final Intent intent = new Intent(TelephonyManager
416                     .ACTION_SUBSCRIPTION_SPECIFIC_CARRIER_IDENTITY_CHANGED);
417             intent.putExtra(TelephonyManager.EXTRA_SPECIFIC_CARRIER_ID, mSpecificCarrierId);
418             intent.putExtra(TelephonyManager.EXTRA_SPECIFIC_CARRIER_NAME, mSpecificCarrierName);
419             intent.putExtra(TelephonyManager.EXTRA_SUBSCRIPTION_ID, mPhone.getSubId());
420             mContext.sendBroadcast(intent);
421 
422             // notify content observers for specific carrier id change event.
423             ContentValues cv = new ContentValues();
424             cv.put(CarrierId.SPECIFIC_CARRIER_ID, mSpecificCarrierId);
425             cv.put(CarrierId.SPECIFIC_CARRIER_ID_NAME, mSpecificCarrierName);
426             mContext.getContentResolver().update(
427                     Telephony.CarrierId.getSpecificCarrierIdUriForSubscriptionId(mPhone.getSubId()),
428                     cv, null, null);
429         }
430 
431         update = false;
432         if (!equals(name, mCarrierName, true)) {
433             logd("[updateCarrierName] from:" + mCarrierName + " to:" + name);
434             mCarrierName = name;
435             update = true;
436         }
437         if (cid != mCarrierId) {
438             logd("[updateCarrierId] from:" + mCarrierId + " to:" + cid);
439             mCarrierId = cid;
440             update = true;
441         }
442         if (mnoCid != mMnoCarrierId) {
443             logd("[updateMnoCarrierId] from:" + mMnoCarrierId + " to:" + mnoCid);
444             mMnoCarrierId = mnoCid;
445             update = true;
446         }
447         if (update) {
448             mCarrierIdLocalLog.log("[updateCarrierIdAndName] cid:" + mCarrierId + " name:"
449                     + mCarrierName + " mnoCid:" + mMnoCarrierId);
450             final Intent intent = new Intent(TelephonyManager
451                     .ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED);
452             intent.putExtra(TelephonyManager.EXTRA_CARRIER_ID, mCarrierId);
453             intent.putExtra(TelephonyManager.EXTRA_CARRIER_NAME, mCarrierName);
454             intent.putExtra(TelephonyManager.EXTRA_SUBSCRIPTION_ID, mPhone.getSubId());
455             mContext.sendBroadcast(intent);
456 
457             // notify content observers for carrier id change event
458             ContentValues cv = new ContentValues();
459             cv.put(CarrierId.CARRIER_ID, mCarrierId);
460             cv.put(CarrierId.CARRIER_NAME, mCarrierName);
461             mContext.getContentResolver().update(
462                     Telephony.CarrierId.getUriForSubscriptionId(mPhone.getSubId()), cv, null, null);
463         }
464         // during esim profile switch, there is no sim absent thus carrier id will persist and
465         // might not trigger an update if switch profiles for the same carrier. thus always update
466         // subscriptioninfo db to make sure we have correct carrier id set.
467         if (SubscriptionManager.isValidSubscriptionId(mPhone.getSubId())) {
468             // only persist carrier id to simInfo db when subId is valid.
469             SubscriptionController.getInstance().setCarrierId(mCarrierId, mPhone.getSubId());
470         }
471     }
472 
makeCarrierMatchingRule(Cursor cursor)473     private static CarrierMatchingRule makeCarrierMatchingRule(Cursor cursor) {
474         String certs = cursor.getString(
475                 cursor.getColumnIndexOrThrow(CarrierId.All.PRIVILEGE_ACCESS_RULE));
476         return new CarrierMatchingRule(
477                 cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.MCCMNC)),
478                 cursor.getString(cursor.getColumnIndexOrThrow(
479                         CarrierId.All.IMSI_PREFIX_XPATTERN)),
480                 cursor.getString(cursor.getColumnIndexOrThrow(
481                         CarrierId.All.ICCID_PREFIX)),
482                 cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.GID1)),
483                 cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.GID2)),
484                 cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.PLMN)),
485                 cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.SPN)),
486                 cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.All.APN)),
487                 (TextUtils.isEmpty(certs) ? null : new ArrayList<>(Arrays.asList(certs))),
488                 cursor.getInt(cursor.getColumnIndexOrThrow(CarrierId.CARRIER_ID)),
489                 cursor.getString(cursor.getColumnIndexOrThrow(CarrierId.CARRIER_NAME)),
490                 cursor.getInt(cursor.getColumnIndexOrThrow(CarrierId.PARENT_CARRIER_ID)));
491     }
492 
493     /**
494      * carrier matching attributes with corresponding cid
495      */
496     public static class CarrierMatchingRule {
497         /**
498          * These scores provide the hierarchical relationship between the attributes, intended to
499          * resolve conflicts in a deterministic way. The scores are constructed such that a match
500          * from a higher tier will beat any subsequent match which does not match at that tier,
501          * so MCCMNC beats everything else. This avoids problems when two (or more) carriers rule
502          * matches as the score helps to find the best match uniquely. e.g.,
503          * rule 1 {mccmnc, imsi} rule 2 {mccmnc, imsi, gid1} and rule 3 {mccmnc, imsi, gid2} all
504          * matches with subscription data. rule 2 wins with the highest matching score.
505          */
506         private static final int SCORE_MCCMNC                   = 1 << 8;
507         private static final int SCORE_IMSI_PREFIX              = 1 << 7;
508         private static final int SCORE_ICCID_PREFIX             = 1 << 6;
509         private static final int SCORE_GID1                     = 1 << 5;
510         private static final int SCORE_GID2                     = 1 << 4;
511         private static final int SCORE_PLMN                     = 1 << 3;
512         private static final int SCORE_PRIVILEGE_ACCESS_RULE    = 1 << 2;
513         private static final int SCORE_SPN                      = 1 << 1;
514         private static final int SCORE_APN                      = 1 << 0;
515 
516         private static final int SCORE_INVALID                  = -1;
517 
518         // carrier matching attributes
519         public final String mccMnc;
520         public final String imsiPrefixPattern;
521         public final String iccidPrefix;
522         public final String gid1;
523         public final String gid2;
524         public final String plmn;
525         public final String spn;
526         public final String apn;
527         // there can be multiple certs configured in the UICC
528         public final List<String> privilegeAccessRule;
529 
530         // user-facing carrier name
531         private String mName;
532         // unique carrier id
533         private int mCid;
534         // unique parent carrier id
535         private int mParentCid;
536 
537         private int mScore = 0;
538 
539         @VisibleForTesting
CarrierMatchingRule(String mccmnc, String imsiPrefixPattern, String iccidPrefix, String gid1, String gid2, String plmn, String spn, String apn, List<String> privilegeAccessRule, int cid, String name, int parentCid)540         public CarrierMatchingRule(String mccmnc, String imsiPrefixPattern, String iccidPrefix,
541                 String gid1, String gid2, String plmn, String spn, String apn,
542                 List<String> privilegeAccessRule, int cid, String name, int parentCid) {
543             mccMnc = mccmnc;
544             this.imsiPrefixPattern = imsiPrefixPattern;
545             this.iccidPrefix = iccidPrefix;
546             this.gid1 = gid1;
547             this.gid2 = gid2;
548             this.plmn = plmn;
549             this.spn = spn;
550             this.apn = apn;
551             this.privilegeAccessRule = privilegeAccessRule;
552             mCid = cid;
553             mName = name;
554             mParentCid = parentCid;
555         }
556 
CarrierMatchingRule(CarrierMatchingRule rule)557         private CarrierMatchingRule(CarrierMatchingRule rule) {
558             mccMnc = rule.mccMnc;
559             imsiPrefixPattern = rule.imsiPrefixPattern;
560             iccidPrefix = rule.iccidPrefix;
561             gid1 = rule.gid1;
562             gid2 = rule.gid2;
563             plmn = rule.plmn;
564             spn = rule.spn;
565             apn = rule.apn;
566             privilegeAccessRule = rule.privilegeAccessRule;
567             mCid = rule.mCid;
568             mName = rule.mName;
569             mParentCid = rule.mParentCid;
570         }
571 
572         // Calculate matching score. Values which aren't set in the rule are considered "wild".
573         // All values in the rule must match in order for the subscription to be considered part of
574         // the carrier. Otherwise, a invalid score -1 will be assigned. A match from a higher tier
575         // will beat any subsequent match which does not match at that tier. When there are multiple
576         // matches at the same tier, the match with highest score will be used.
match(CarrierMatchingRule subscriptionRule)577         public void match(CarrierMatchingRule subscriptionRule) {
578             mScore = 0;
579             if (mccMnc != null) {
580                 if (!CarrierResolver.equals(subscriptionRule.mccMnc, mccMnc, false)) {
581                     mScore = SCORE_INVALID;
582                     return;
583                 }
584                 mScore += SCORE_MCCMNC;
585             }
586             if (imsiPrefixPattern != null) {
587                 if (!imsiPrefixMatch(subscriptionRule.imsiPrefixPattern, imsiPrefixPattern)) {
588                     mScore = SCORE_INVALID;
589                     return;
590                 }
591                 mScore += SCORE_IMSI_PREFIX;
592             }
593             if (iccidPrefix != null) {
594                 if (!iccidPrefixMatch(subscriptionRule.iccidPrefix, iccidPrefix)) {
595                     mScore = SCORE_INVALID;
596                     return;
597                 }
598                 mScore += SCORE_ICCID_PREFIX;
599             }
600             if (gid1 != null) {
601                 if (!gidMatch(subscriptionRule.gid1, gid1)) {
602                     mScore = SCORE_INVALID;
603                     return;
604                 }
605                 mScore += SCORE_GID1;
606             }
607             if (gid2 != null) {
608                 if (!gidMatch(subscriptionRule.gid2, gid2)) {
609                     mScore = SCORE_INVALID;
610                     return;
611                 }
612                 mScore += SCORE_GID2;
613             }
614             if (plmn != null) {
615                 if (!CarrierResolver.equals(subscriptionRule.plmn, plmn, true)) {
616                     mScore = SCORE_INVALID;
617                     return;
618                 }
619                 mScore += SCORE_PLMN;
620             }
621             if (spn != null) {
622                 if (!CarrierResolver.equals(subscriptionRule.spn, spn, true)) {
623                     mScore = SCORE_INVALID;
624                     return;
625                 }
626                 mScore += SCORE_SPN;
627             }
628 
629             if (privilegeAccessRule != null && !privilegeAccessRule.isEmpty()) {
630                 if (!carrierPrivilegeRulesMatch(subscriptionRule.privilegeAccessRule,
631                         privilegeAccessRule)) {
632                     mScore = SCORE_INVALID;
633                     return;
634                 }
635                 mScore += SCORE_PRIVILEGE_ACCESS_RULE;
636             }
637 
638             if (apn != null) {
639                 if (!CarrierResolver.equals(subscriptionRule.apn, apn, true)) {
640                     mScore = SCORE_INVALID;
641                     return;
642                 }
643                 mScore += SCORE_APN;
644             }
645         }
646 
imsiPrefixMatch(String imsi, String prefixXPattern)647         private boolean imsiPrefixMatch(String imsi, String prefixXPattern) {
648             if (TextUtils.isEmpty(prefixXPattern)) return true;
649             if (TextUtils.isEmpty(imsi)) return false;
650             if (imsi.length() < prefixXPattern.length()) {
651                 return false;
652             }
653             for (int i = 0; i < prefixXPattern.length(); i++) {
654                 if ((prefixXPattern.charAt(i) != 'x') && (prefixXPattern.charAt(i) != 'X')
655                         && (prefixXPattern.charAt(i) != imsi.charAt(i))) {
656                     return false;
657                 }
658             }
659             return true;
660         }
661 
iccidPrefixMatch(String iccid, String prefix)662         private boolean iccidPrefixMatch(String iccid, String prefix) {
663             if (iccid == null || prefix == null) {
664                 return false;
665             }
666             return iccid.startsWith(prefix);
667         }
668 
669         // We are doing prefix and case insensitive match.
670         // Ideally we should do full string match. However due to SIM manufacture issues
671         // gid from some SIM might has garbage tail.
gidMatch(String gidFromSim, String gid)672         private boolean gidMatch(String gidFromSim, String gid) {
673             return (gidFromSim != null) && gidFromSim.toLowerCase().startsWith(gid.toLowerCase());
674         }
675 
carrierPrivilegeRulesMatch(List<String> certsFromSubscription, List<String> certs)676         private boolean carrierPrivilegeRulesMatch(List<String> certsFromSubscription,
677                                                    List<String> certs) {
678             if (certsFromSubscription == null || certsFromSubscription.isEmpty()) {
679                 return false;
680             }
681             for (String cert : certs) {
682                 for (String certFromSubscription : certsFromSubscription) {
683                     if (!TextUtils.isEmpty(cert)
684                             && cert.equalsIgnoreCase(certFromSubscription)) {
685                         return true;
686                     }
687                 }
688             }
689             return false;
690         }
691 
toString()692         public String toString() {
693             return "[CarrierMatchingRule] -"
694                     + " mccmnc: " + mccMnc
695                     + " gid1: " + gid1
696                     + " gid2: " + gid2
697                     + " plmn: " + plmn
698                     + " imsi_prefix: " + imsiPrefixPattern
699                     + " iccid_prefix" + iccidPrefix
700                     + " spn: " + spn
701                     + " privilege_access_rule: " + privilegeAccessRule
702                     + " apn: " + apn
703                     + " name: " + mName
704                     + " cid: " + mCid
705                     + " score: " + mScore;
706         }
707     }
708 
getSubscriptionMatchingRule()709     private CarrierMatchingRule getSubscriptionMatchingRule() {
710         final String mccmnc = mTelephonyMgr.getSimOperatorNumericForPhone(mPhone.getPhoneId());
711         final String iccid = mPhone.getIccSerialNumber();
712         final String gid1 = mPhone.getGroupIdLevel1();
713         final String gid2 = mPhone.getGroupIdLevel2();
714         final String imsi = mPhone.getSubscriberId();
715         final String plmn = mPhone.getPlmn();
716         final String spn = mSpn;
717         final String apn = mPreferApn;
718         List<String> accessRules;
719         // check if test override present
720         if (!TextUtils.isEmpty(mTestOverrideCarrierPriviledgeRule)) {
721             accessRules = new ArrayList<>(Arrays.asList(mTestOverrideCarrierPriviledgeRule));
722         } else {
723             accessRules = mTelephonyMgr.createForSubscriptionId(mPhone.getSubId())
724                     .getCertsFromCarrierPrivilegeAccessRules();
725         }
726 
727         if (VDBG) {
728             logd("[matchSubscriptionCarrier]"
729                     + " mnnmnc:" + mccmnc
730                     + " gid1: " + gid1
731                     + " gid2: " + gid2
732                     + " imsi: " + Rlog.pii(LOG_TAG, imsi)
733                     + " iccid: " + Rlog.pii(LOG_TAG, iccid)
734                     + " plmn: " + plmn
735                     + " spn: " + spn
736                     + " apn: " + apn
737                     + " accessRules: " + ((accessRules != null) ? accessRules : null));
738         }
739         return new CarrierMatchingRule(
740                 mccmnc, imsi, iccid, gid1, gid2, plmn, spn, apn, accessRules,
741                 TelephonyManager.UNKNOWN_CARRIER_ID, null,
742                 TelephonyManager.UNKNOWN_CARRIER_ID);
743     }
744 
745     /**
746      * find the best matching carrier from candidates with matched subscription MCCMNC.
747      */
matchSubscriptionCarrier()748     private void matchSubscriptionCarrier() {
749         if (!SubscriptionManager.isValidSubscriptionId(mPhone.getSubId())) {
750             logd("[matchSubscriptionCarrier]" + "skip before sim records loaded");
751             return;
752         }
753         int maxScore = CarrierMatchingRule.SCORE_INVALID;
754         /**
755          * For child-parent relationship. either child and parent have the same matching
756          * score, or child's matching score > parents' matching score.
757          */
758         CarrierMatchingRule maxRule = null;
759         CarrierMatchingRule maxRuleParent = null;
760         /**
761          * matching rule with mccmnc only. If mnoRule is found, then mno carrier id equals to the
762          * cid from mnoRule. otherwise, mno carrier id is same as cid.
763          */
764         CarrierMatchingRule mnoRule = null;
765         CarrierMatchingRule subscriptionRule = getSubscriptionMatchingRule();
766 
767         for (CarrierMatchingRule rule : mCarrierMatchingRulesOnMccMnc) {
768             rule.match(subscriptionRule);
769             if (rule.mScore > maxScore) {
770                 maxScore = rule.mScore;
771                 maxRule = rule;
772                 maxRuleParent = rule;
773             } else if (maxScore > CarrierMatchingRule.SCORE_INVALID && rule.mScore == maxScore) {
774                 // to handle the case that child parent has the same matching score, we need to
775                 // differentiate who is child who is parent.
776                 if (rule.mParentCid == maxRule.mCid) {
777                     maxRule = rule;
778                 } else if (maxRule.mParentCid == rule.mCid) {
779                     maxRuleParent = rule;
780                 }
781             }
782             if (rule.mScore == CarrierMatchingRule.SCORE_MCCMNC) {
783                 mnoRule = rule;
784             }
785         }
786         if (maxScore == CarrierMatchingRule.SCORE_INVALID) {
787             logd("[matchSubscriptionCarrier - no match] cid: " + TelephonyManager.UNKNOWN_CARRIER_ID
788                     + " name: " + null);
789             updateCarrierIdAndName(TelephonyManager.UNKNOWN_CARRIER_ID, null,
790                     TelephonyManager.UNKNOWN_CARRIER_ID, null,
791                     TelephonyManager.UNKNOWN_CARRIER_ID);
792         } else {
793             // if there is a single matching result, check if this rule has parent cid assigned.
794             if ((maxRule == maxRuleParent)
795                     && maxRule.mParentCid != TelephonyManager.UNKNOWN_CARRIER_ID) {
796                 maxRuleParent = new CarrierMatchingRule(maxRule);
797                 maxRuleParent.mCid = maxRuleParent.mParentCid;
798                 maxRuleParent.mName = getCarrierNameFromId(maxRuleParent.mCid);
799             }
800             logd("[matchSubscriptionCarrier] specific cid: " + maxRule.mCid
801                     + " specific name: " + maxRule.mName +" cid: " + maxRuleParent.mCid
802                     + " name: " + maxRuleParent.mName);
803             updateCarrierIdAndName(maxRuleParent.mCid, maxRuleParent.mName,
804                     maxRule.mCid, maxRule.mName,
805                     (mnoRule == null) ? maxRule.mCid : mnoRule.mCid);
806         }
807 
808         /*
809          * Write Carrier Identification Matching event, logging with the
810          * carrierId, mccmnc, gid1 and carrier list version to differentiate below cases of metrics:
811          * 1) unknown mccmnc - the Carrier Id provider contains no rule that matches the
812          * read mccmnc.
813          * 2) the Carrier Id provider contains some rule(s) that match the read mccmnc,
814          * but the read gid1 is not matched within the highest-scored rule.
815          * 3) successfully found a matched carrier id in the provider.
816          * 4) use carrier list version to compare the unknown carrier ratio between each version.
817          */
818         String unknownGid1ToLog = ((maxScore & CarrierMatchingRule.SCORE_GID1) == 0
819                 && !TextUtils.isEmpty(subscriptionRule.gid1)) ? subscriptionRule.gid1 : null;
820         String unknownMccmncToLog = ((maxScore == CarrierMatchingRule.SCORE_INVALID
821                 || (maxScore & CarrierMatchingRule.SCORE_GID1) == 0)
822                 && !TextUtils.isEmpty(subscriptionRule.mccMnc)) ? subscriptionRule.mccMnc : null;
823 
824         // pass subscription rule to metrics. scrub all possible PII before uploading.
825         // only log apn if not user edited.
826         String apn = (subscriptionRule.apn != null
827                 && !isPreferApnUserEdited(subscriptionRule.apn))
828                 ? subscriptionRule.apn : null;
829         // only log first 7 bits of iccid
830         String iccidPrefix = (subscriptionRule.iccidPrefix != null)
831                 && (subscriptionRule.iccidPrefix.length() >= 7)
832                 ? subscriptionRule.iccidPrefix.substring(0, 7) : subscriptionRule.iccidPrefix;
833         // only log first 8 bits of imsi
834         String imsiPrefix = (subscriptionRule.imsiPrefixPattern != null)
835                 && (subscriptionRule.imsiPrefixPattern.length() >= 8)
836                 ? subscriptionRule.imsiPrefixPattern.substring(0, 8)
837                 : subscriptionRule.imsiPrefixPattern;
838 
839         CarrierMatchingRule simInfo = new CarrierMatchingRule(
840                 subscriptionRule.mccMnc,
841                 imsiPrefix,
842                 iccidPrefix,
843                 subscriptionRule.gid1,
844                 subscriptionRule.gid2,
845                 subscriptionRule.plmn,
846                 subscriptionRule.spn,
847                 apn,
848                 subscriptionRule.privilegeAccessRule,
849                 -1, null, -1);
850 
851         TelephonyMetrics.getInstance().writeCarrierIdMatchingEvent(
852                 mPhone.getPhoneId(), getCarrierListVersion(), mCarrierId,
853                 unknownMccmncToLog, unknownGid1ToLog, simInfo);
854     }
855 
getCarrierListVersion()856     public int getCarrierListVersion() {
857         final Cursor cursor = mContext.getContentResolver().query(
858                 Uri.withAppendedPath(CarrierId.All.CONTENT_URI,
859                 "get_version"), null, null, null);
860         cursor.moveToFirst();
861         return cursor.getInt(0);
862     }
863 
getCarrierId()864     public int getCarrierId() {
865         return mCarrierId;
866     }
867     /**
868      * Returns fine-grained carrier id of the current subscription. Carrier ids with a valid parent
869      * id are specific carrier ids.
870      *
871      * A specific carrier ID can represent the fact that a carrier may be in effect an aggregation
872      * of other carriers (ie in an MVNO type scenario) where each of these specific carriers which
873      * are used to make up the actual carrier service may have different carrier configurations.
874      * A specific carrier ID could also be used, for example, in a scenario where a carrier requires
875      * different carrier configuration for different service offering such as a prepaid plan.
876      * e.g, {@link #getCarrierId()} will always return Tracfone (id 2022) for a Tracfone SIM, while
877      * {@link #getSpecificCarrierId()} can return Tracfone AT&T or Tracfone T-Mobile based on the
878      * IMSI from the current subscription.
879      *
880      * For carriers without any fine-grained carrier ids, return {@link #getCarrierId()}
881      */
getSpecificCarrierId()882     public int getSpecificCarrierId() {
883         return mSpecificCarrierId;
884     }
885 
getCarrierName()886     public String getCarrierName() {
887         return mCarrierName;
888     }
889 
getSpecificCarrierName()890     public String getSpecificCarrierName() {
891         return mSpecificCarrierName;
892     }
893 
getMnoCarrierId()894     public int getMnoCarrierId() {
895         return mMnoCarrierId;
896     }
897 
898     /**
899      * a util function to convert carrierIdentifier to the best matching carrier id.
900      *
901      * @return the best matching carrier id.
902      */
getCarrierIdFromIdentifier(@onNull Context context, @NonNull CarrierIdentifier carrierIdentifier)903     public static int getCarrierIdFromIdentifier(@NonNull Context context,
904                                                  @NonNull CarrierIdentifier carrierIdentifier) {
905         final String mccmnc = carrierIdentifier.getMcc() + carrierIdentifier.getMnc();
906         final String gid1 = carrierIdentifier.getGid1();
907         final String gid2 = carrierIdentifier.getGid2();
908         final String imsi = carrierIdentifier.getImsi();
909         final String spn = carrierIdentifier.getSpn();
910         if (VDBG) {
911             logd("[getCarrierIdFromIdentifier]"
912                     + " mnnmnc:" + mccmnc
913                     + " gid1: " + gid1
914                     + " gid2: " + gid2
915                     + " imsi: " + Rlog.pii(LOG_TAG, imsi)
916                     + " spn: " + spn);
917         }
918         // assign null to other fields which are not supported by carrierIdentifier.
919         CarrierMatchingRule targetRule =
920                 new CarrierMatchingRule(mccmnc, imsi, null, gid1, gid2, null,
921                         spn, null, null,
922                         TelephonyManager.UNKNOWN_CARRIER_ID_LIST_VERSION, null,
923                         TelephonyManager.UNKNOWN_CARRIER_ID);
924 
925         int carrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
926         int maxScore = CarrierMatchingRule.SCORE_INVALID;
927         List<CarrierMatchingRule> rules = getCarrierMatchingRulesFromMccMnc(
928                 context, targetRule.mccMnc);
929         for (CarrierMatchingRule rule : rules) {
930             rule.match(targetRule);
931             if (rule.mScore > maxScore) {
932                 maxScore = rule.mScore;
933                 carrierId = rule.mCid;
934             }
935         }
936         return carrierId;
937     }
938 
939     /**
940      * a util function to convert {mccmnc, mvno_type, mvno_data} to all matching carrier ids.
941      *
942      * @return a list of id with matching {mccmnc, mvno_type, mvno_data}
943      */
getCarrierIdsFromApnQuery(@onNull Context context, String mccmnc, String mvnoCase, String mvnoData)944     public static List<Integer> getCarrierIdsFromApnQuery(@NonNull Context context,
945                                                           String mccmnc, String mvnoCase,
946                                                           String mvnoData) {
947         String selection = CarrierId.All.MCCMNC + "=" + mccmnc;
948         // build the proper query
949         if ("spn".equals(mvnoCase) && mvnoData != null) {
950             selection += " AND " + CarrierId.All.SPN + "='" + mvnoData + "'";
951         } else if ("imsi".equals(mvnoCase) && mvnoData != null) {
952             selection += " AND " + CarrierId.All.IMSI_PREFIX_XPATTERN + "='" + mvnoData + "'";
953         } else if ("gid1".equals(mvnoCase) && mvnoData != null) {
954             selection += " AND " + CarrierId.All.GID1 + "='" + mvnoData + "'";
955         } else if ("gid2".equals(mvnoCase) && mvnoData != null) {
956             selection += " AND " + CarrierId.All.GID2 + "='" + mvnoData +"'";
957         } else {
958             logd("mvno case empty or other invalid values");
959         }
960 
961         List<Integer> ids = new ArrayList<>();
962         try {
963             Cursor cursor = context.getContentResolver().query(
964                     CarrierId.All.CONTENT_URI,
965                     /* projection */ null,
966                     /* selection */ selection,
967                     /* selectionArgs */ null, null);
968             try {
969                 if (cursor != null) {
970                     if (VDBG) {
971                         logd("[getCarrierIdsFromApnQuery]- " + cursor.getCount()
972                                 + " Records(s) in DB");
973                     }
974                     while (cursor.moveToNext()) {
975                         int cid = cursor.getInt(cursor.getColumnIndex(CarrierId.CARRIER_ID));
976                         if (!ids.contains(cid)) {
977                             ids.add(cid);
978                         }
979                     }
980                 }
981             } finally {
982                 if (cursor != null) {
983                     cursor.close();
984                 }
985             }
986         } catch (Exception ex) {
987             loge("[getCarrierIdsFromApnQuery]- ex: " + ex);
988         }
989         logd(selection + " " + ids);
990         return ids;
991     }
992 
993     // static helper function to get carrier id from mccmnc
getCarrierIdFromMccMnc(@onNull Context context, String mccmnc)994     public static int getCarrierIdFromMccMnc(@NonNull Context context, String mccmnc) {
995         try {
996             Cursor cursor = context.getContentResolver().query(
997                     CarrierId.All.CONTENT_URI,
998                     /* projection */ null,
999                     /* selection */ CarrierId.All.MCCMNC + "=? AND "
1000                             + CarrierId.All.GID1 + " is NULL AND "
1001                             + CarrierId.All.GID2 + " is NULL AND "
1002                             + CarrierId.All.IMSI_PREFIX_XPATTERN + " is NULL AND "
1003                             + CarrierId.All.SPN + " is NULL AND "
1004                             + CarrierId.All.ICCID_PREFIX + " is NULL AND "
1005                             + CarrierId.All.PLMN + " is NULL AND "
1006                             + CarrierId.All.PRIVILEGE_ACCESS_RULE + " is NULL AND "
1007                             + CarrierId.All.APN + " is NULL",
1008                     /* selectionArgs */ new String[]{mccmnc},
1009                     null);
1010             try {
1011                 if (cursor != null) {
1012                     if (VDBG) {
1013                         logd("[getCarrierIdFromMccMnc]- " + cursor.getCount()
1014                                 + " Records(s) in DB" + " mccmnc: " + mccmnc);
1015                     }
1016                     while (cursor.moveToNext()) {
1017                         return cursor.getInt(cursor.getColumnIndex(CarrierId.CARRIER_ID));
1018                     }
1019                 }
1020             } finally {
1021                 if (cursor != null) {
1022                     cursor.close();
1023                 }
1024             }
1025         } catch (Exception ex) {
1026             loge("[getCarrierIdFromMccMnc]- ex: " + ex);
1027         }
1028         return TelephonyManager.UNKNOWN_CARRIER_ID;
1029     }
1030 
equals(String a, String b, boolean ignoreCase)1031     private static boolean equals(String a, String b, boolean ignoreCase) {
1032         if (a == null && b == null) return true;
1033         if (a != null && b != null) {
1034             return (ignoreCase) ? a.equalsIgnoreCase(b) : a.equals(b);
1035         }
1036         return false;
1037     }
1038 
logd(String str)1039     private static void logd(String str) {
1040         Rlog.d(LOG_TAG, str);
1041     }
loge(String str)1042     private static void loge(String str) {
1043         Rlog.e(LOG_TAG, str);
1044     }
dump(FileDescriptor fd, PrintWriter pw, String[] args)1045     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1046         final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
1047         ipw.println("mCarrierResolverLocalLogs:");
1048         ipw.increaseIndent();
1049         mCarrierIdLocalLog.dump(fd, pw, args);
1050         ipw.decreaseIndent();
1051 
1052         ipw.println("mCarrierId: " + mCarrierId);
1053         ipw.println("mSpecificCarrierId: " + mSpecificCarrierId);
1054         ipw.println("mMnoCarrierId: " + mMnoCarrierId);
1055         ipw.println("mCarrierName: " + mCarrierName);
1056         ipw.println("mSpecificCarrierName: " + mSpecificCarrierName);
1057         ipw.println("carrier_list_version: " + getCarrierListVersion());
1058 
1059         ipw.println("mCarrierMatchingRules on mccmnc: "
1060                 + mTelephonyMgr.getSimOperatorNumericForPhone(mPhone.getPhoneId()));
1061         ipw.increaseIndent();
1062         for (CarrierMatchingRule rule : mCarrierMatchingRulesOnMccMnc) {
1063             ipw.println(rule.toString());
1064         }
1065         ipw.decreaseIndent();
1066 
1067         ipw.println("mSpn: " + mSpn);
1068         ipw.println("mPreferApn: " + mPreferApn);
1069         ipw.flush();
1070     }
1071 }
1072