1 /*
2  * Copyright (C) 2023 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.server.thread;
18 
19 import static com.android.server.thread.ThreadPersistentSettings.THREAD_COUNTRY_CODE;
20 
21 import android.annotation.Nullable;
22 import android.annotation.StringDef;
23 import android.annotation.TargetApi;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.location.Address;
29 import android.location.Geocoder;
30 import android.location.Location;
31 import android.location.LocationManager;
32 import android.net.thread.IOperationReceiver;
33 import android.net.wifi.WifiManager;
34 import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
35 import android.os.Build;
36 import android.sysprop.ThreadNetworkProperties;
37 import android.telephony.SubscriptionInfo;
38 import android.telephony.SubscriptionManager;
39 import android.telephony.TelephonyManager;
40 import android.util.ArrayMap;
41 import android.util.Log;
42 
43 import com.android.connectivity.resources.R;
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.server.connectivity.ConnectivityResources;
46 
47 import java.io.FileDescriptor;
48 import java.io.PrintWriter;
49 import java.lang.annotation.Retention;
50 import java.lang.annotation.RetentionPolicy;
51 import java.time.Instant;
52 import java.util.List;
53 import java.util.Locale;
54 import java.util.Map;
55 import java.util.Objects;
56 
57 /**
58  * Provide functions for making changes to Thread Network country code. This Country Code is from
59  * location, WiFi, telephony or OEM configuration. This class sends Country Code to Thread Network
60  * native layer.
61  *
62  * <p>This class is thread-safe.
63  */
64 @TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
65 public class ThreadNetworkCountryCode {
66     private static final String TAG = "ThreadNetworkCountryCode";
67     // To be used when there is no country code available.
68     @VisibleForTesting public static final String DEFAULT_COUNTRY_CODE = "WW";
69 
70     // Wait 1 hour between updates.
71     private static final long TIME_BETWEEN_LOCATION_UPDATES_MS = 1000L * 60 * 60 * 1;
72     // Minimum distance before an update is triggered, in meters. We don't need this to be too
73     // exact because all we care about is what country the user is in.
74     private static final float DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS = 5_000.0f;
75 
76     /** List of country code sources. */
77     @Retention(RetentionPolicy.SOURCE)
78     @StringDef(
79             prefix = "COUNTRY_CODE_SOURCE_",
80             value = {
81                 COUNTRY_CODE_SOURCE_DEFAULT,
82                 COUNTRY_CODE_SOURCE_LOCATION,
83                 COUNTRY_CODE_SOURCE_OEM,
84                 COUNTRY_CODE_SOURCE_OVERRIDE,
85                 COUNTRY_CODE_SOURCE_TELEPHONY,
86                 COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
87                 COUNTRY_CODE_SOURCE_WIFI,
88                 COUNTRY_CODE_SOURCE_SETTINGS,
89             })
90     private @interface CountryCodeSource {}
91 
92     private static final String COUNTRY_CODE_SOURCE_DEFAULT = "Default";
93     private static final String COUNTRY_CODE_SOURCE_LOCATION = "Location";
94     private static final String COUNTRY_CODE_SOURCE_OEM = "Oem";
95     private static final String COUNTRY_CODE_SOURCE_OVERRIDE = "Override";
96     private static final String COUNTRY_CODE_SOURCE_TELEPHONY = "Telephony";
97     private static final String COUNTRY_CODE_SOURCE_TELEPHONY_LAST = "TelephonyLast";
98     private static final String COUNTRY_CODE_SOURCE_WIFI = "Wifi";
99     private static final String COUNTRY_CODE_SOURCE_SETTINGS = "Settings";
100 
101     private static final CountryCodeInfo DEFAULT_COUNTRY_CODE_INFO =
102             new CountryCodeInfo(DEFAULT_COUNTRY_CODE, COUNTRY_CODE_SOURCE_DEFAULT);
103 
104     private final ConnectivityResources mResources;
105     private final Context mContext;
106     private final LocationManager mLocationManager;
107     @Nullable private final Geocoder mGeocoder;
108     private final ThreadNetworkControllerService mThreadNetworkControllerService;
109     private final WifiManager mWifiManager;
110     private final TelephonyManager mTelephonyManager;
111     private final SubscriptionManager mSubscriptionManager;
112     private final Map<Integer, TelephonyCountryCodeSlotInfo> mTelephonyCountryCodeSlotInfoMap =
113             new ArrayMap();
114     private final ThreadPersistentSettings mPersistentSettings;
115 
116     @Nullable private CountryCodeInfo mCurrentCountryCodeInfo;
117     @Nullable private CountryCodeInfo mLocationCountryCodeInfo;
118     @Nullable private CountryCodeInfo mOverrideCountryCodeInfo;
119     @Nullable private CountryCodeInfo mWifiCountryCodeInfo;
120     @Nullable private CountryCodeInfo mTelephonyCountryCodeInfo;
121     @Nullable private CountryCodeInfo mTelephonyLastCountryCodeInfo;
122     @Nullable private CountryCodeInfo mOemCountryCodeInfo;
123 
124     /** Container class to store Thread country code information. */
125     private static final class CountryCodeInfo {
126         private String mCountryCode;
127         @CountryCodeSource private String mSource;
128         private final Instant mUpdatedTimestamp;
129 
130         /**
131          * Constructs a new {@code CountryCodeInfo} from the given country code, country code source
132          * and country coode created time.
133          *
134          * @param countryCode a String representation of the country code as defined in ISO 3166.
135          * @param countryCodeSource a String representation of country code source.
136          * @param instant a Instant representation of the time when the country code was created.
137          * @throws IllegalArgumentException if {@code countryCode} contains invalid country code.
138          */
CountryCodeInfo( String countryCode, @CountryCodeSource String countryCodeSource, Instant instant)139         public CountryCodeInfo(
140                 String countryCode, @CountryCodeSource String countryCodeSource, Instant instant) {
141             if (!isValidCountryCode(countryCode)) {
142                 throw new IllegalArgumentException("Country code is invalid: " + countryCode);
143             }
144 
145             mCountryCode = countryCode;
146             mSource = countryCodeSource;
147             mUpdatedTimestamp = instant;
148         }
149 
150         /**
151          * Constructs a new {@code CountryCodeInfo} from the given country code, country code
152          * source. The updated timestamp of the country code will be set to the time when {@code
153          * CountryCodeInfo} was constructed.
154          *
155          * @param countryCode a String representation of the country code as defined in ISO 3166.
156          * @param countryCodeSource a String representation of country code source.
157          * @throws IllegalArgumentException if {@code countryCode} contains invalid country code.
158          */
CountryCodeInfo(String countryCode, @CountryCodeSource String countryCodeSource)159         public CountryCodeInfo(String countryCode, @CountryCodeSource String countryCodeSource) {
160             this(countryCode, countryCodeSource, Instant.now());
161         }
162 
getCountryCode()163         public String getCountryCode() {
164             return mCountryCode;
165         }
166 
isCountryCodeMatch(CountryCodeInfo countryCodeInfo)167         public boolean isCountryCodeMatch(CountryCodeInfo countryCodeInfo) {
168             if (countryCodeInfo == null) {
169                 return false;
170             }
171 
172             return Objects.equals(countryCodeInfo.mCountryCode, mCountryCode);
173         }
174 
175         @Override
toString()176         public String toString() {
177             return "CountryCodeInfo{ mCountryCode: "
178                     + mCountryCode
179                     + ", mSource: "
180                     + mSource
181                     + ", mUpdatedTimestamp: "
182                     + mUpdatedTimestamp
183                     + "}";
184         }
185     }
186 
187     /** Container class to store country code per SIM slot. */
188     private static final class TelephonyCountryCodeSlotInfo {
189         public int slotIndex;
190         public String countryCode;
191         public String lastKnownCountryCode;
192         public Instant timestamp;
193 
194         @Override
toString()195         public String toString() {
196             return "TelephonyCountryCodeSlotInfo{ slotIndex: "
197                     + slotIndex
198                     + ", countryCode: "
199                     + countryCode
200                     + ", lastKnownCountryCode: "
201                     + lastKnownCountryCode
202                     + ", timestamp: "
203                     + timestamp
204                     + "}";
205         }
206     }
207 
isLocationUseForCountryCodeEnabled()208     private boolean isLocationUseForCountryCodeEnabled() {
209         return mResources
210                 .get()
211                 .getBoolean(R.bool.config_thread_location_use_for_country_code_enabled);
212     }
213 
ThreadNetworkCountryCode( LocationManager locationManager, ThreadNetworkControllerService threadNetworkControllerService, @Nullable Geocoder geocoder, ConnectivityResources resources, WifiManager wifiManager, Context context, TelephonyManager telephonyManager, SubscriptionManager subscriptionManager, @Nullable String oemCountryCode, ThreadPersistentSettings persistentSettings)214     public ThreadNetworkCountryCode(
215             LocationManager locationManager,
216             ThreadNetworkControllerService threadNetworkControllerService,
217             @Nullable Geocoder geocoder,
218             ConnectivityResources resources,
219             WifiManager wifiManager,
220             Context context,
221             TelephonyManager telephonyManager,
222             SubscriptionManager subscriptionManager,
223             @Nullable String oemCountryCode,
224             ThreadPersistentSettings persistentSettings) {
225         mLocationManager = locationManager;
226         mThreadNetworkControllerService = threadNetworkControllerService;
227         mGeocoder = geocoder;
228         mResources = resources;
229         mWifiManager = wifiManager;
230         mContext = context;
231         mTelephonyManager = telephonyManager;
232         mSubscriptionManager = subscriptionManager;
233         mPersistentSettings = persistentSettings;
234 
235         if (oemCountryCode != null) {
236             mOemCountryCodeInfo = new CountryCodeInfo(oemCountryCode, COUNTRY_CODE_SOURCE_OEM);
237         }
238 
239         mCurrentCountryCodeInfo = pickCountryCode();
240     }
241 
newInstance( Context context, ThreadNetworkControllerService controllerService, ThreadPersistentSettings persistentSettings)242     public static ThreadNetworkCountryCode newInstance(
243             Context context,
244             ThreadNetworkControllerService controllerService,
245             ThreadPersistentSettings persistentSettings) {
246         return new ThreadNetworkCountryCode(
247                 context.getSystemService(LocationManager.class),
248                 controllerService,
249                 Geocoder.isPresent() ? new Geocoder(context) : null,
250                 new ConnectivityResources(context),
251                 context.getSystemService(WifiManager.class),
252                 context,
253                 context.getSystemService(TelephonyManager.class),
254                 context.getSystemService(SubscriptionManager.class),
255                 ThreadNetworkProperties.country_code().orElse(null),
256                 persistentSettings);
257     }
258 
259     /** Sets up this country code module to listen to location country code changes. */
initialize()260     public synchronized void initialize() {
261         registerGeocoderCountryCodeCallback();
262         registerWifiCountryCodeCallback();
263         registerTelephonyCountryCodeCallback();
264         updateTelephonyCountryCodeFromSimCard();
265         updateCountryCode(false /* forceUpdate */);
266     }
267 
registerGeocoderCountryCodeCallback()268     private synchronized void registerGeocoderCountryCodeCallback() {
269         if ((mGeocoder != null) && isLocationUseForCountryCodeEnabled()) {
270             mLocationManager.requestLocationUpdates(
271                     LocationManager.PASSIVE_PROVIDER,
272                     TIME_BETWEEN_LOCATION_UPDATES_MS,
273                     DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS,
274                     location -> setCountryCodeFromGeocodingLocation(location));
275         }
276     }
277 
geocodeListener(List<Address> addresses)278     private synchronized void geocodeListener(List<Address> addresses) {
279         if (addresses != null && !addresses.isEmpty()) {
280             String countryCode = addresses.get(0).getCountryCode();
281 
282             if (isValidCountryCode(countryCode)) {
283                 Log.d(TAG, "Set location country code to: " + countryCode);
284                 mLocationCountryCodeInfo =
285                         new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_LOCATION);
286             } else {
287                 Log.d(TAG, "Received invalid location country code");
288                 mLocationCountryCodeInfo = null;
289             }
290 
291             updateCountryCode(false /* forceUpdate */);
292         }
293     }
294 
setCountryCodeFromGeocodingLocation(@ullable Location location)295     private synchronized void setCountryCodeFromGeocodingLocation(@Nullable Location location) {
296         if ((location == null) || (mGeocoder == null)) return;
297 
298         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
299             Log.wtf(
300                     TAG,
301                     "Unexpected call to set country code from the Geocoding location, "
302                             + "Thread code never runs under T or lower.");
303             return;
304         }
305 
306         mGeocoder.getFromLocation(
307                 location.getLatitude(),
308                 location.getLongitude(),
309                 1 /* maxResults */,
310                 this::geocodeListener);
311     }
312 
registerWifiCountryCodeCallback()313     private synchronized void registerWifiCountryCodeCallback() {
314         if (mWifiManager != null) {
315             mWifiManager.registerActiveCountryCodeChangedCallback(
316                     r -> r.run(), new WifiCountryCodeCallback());
317         }
318     }
319 
320     private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback {
321         @Override
onActiveCountryCodeChanged(String countryCode)322         public void onActiveCountryCodeChanged(String countryCode) {
323             Log.d(TAG, "Wifi country code is changed to " + countryCode);
324             synchronized ("ThreadNetworkCountryCode.this") {
325                 if (isValidCountryCode(countryCode)) {
326                     mWifiCountryCodeInfo =
327                             new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_WIFI);
328                 } else {
329                     Log.w(TAG, "WiFi country code " + countryCode + " is invalid");
330                     mWifiCountryCodeInfo = null;
331                 }
332 
333                 updateCountryCode(false /* forceUpdate */);
334             }
335         }
336 
337         @Override
onCountryCodeInactive()338         public void onCountryCodeInactive() {
339             Log.d(TAG, "Wifi country code is inactived");
340             synchronized ("ThreadNetworkCountryCode.this") {
341                 mWifiCountryCodeInfo = null;
342                 updateCountryCode(false /* forceUpdate */);
343             }
344         }
345     }
346 
registerTelephonyCountryCodeCallback()347     private synchronized void registerTelephonyCountryCodeCallback() {
348         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
349             Log.wtf(
350                     TAG,
351                     "Unexpected call to register the telephony country code changed callback, "
352                             + "Thread code never runs under T or lower.");
353             return;
354         }
355 
356         BroadcastReceiver broadcastReceiver =
357                 new BroadcastReceiver() {
358                     @Override
359                     public void onReceive(Context context, Intent intent) {
360                         int slotIndex =
361                                 intent.getIntExtra(
362                                         SubscriptionManager.EXTRA_SLOT_INDEX,
363                                         SubscriptionManager.INVALID_SIM_SLOT_INDEX);
364                         String lastKnownCountryCode = null;
365                         String countryCode =
366                                 intent.getStringExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY);
367 
368                         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
369                             lastKnownCountryCode =
370                                     intent.getStringExtra(
371                                             TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY);
372                         }
373 
374                         setTelephonyCountryCodeAndLastKnownCountryCode(
375                                 slotIndex, countryCode, lastKnownCountryCode);
376                     }
377                 };
378 
379         mContext.registerReceiver(
380                 broadcastReceiver,
381                 new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED),
382                 Context.RECEIVER_EXPORTED);
383     }
384 
updateTelephonyCountryCodeFromSimCard()385     private synchronized void updateTelephonyCountryCodeFromSimCard() {
386         List<SubscriptionInfo> subscriptionInfoList =
387                 mSubscriptionManager.getActiveSubscriptionInfoList();
388 
389         if (subscriptionInfoList == null) {
390             Log.d(TAG, "No SIM card is found");
391             return;
392         }
393 
394         for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) {
395             String countryCode;
396             int slotIndex;
397 
398             slotIndex = subscriptionInfo.getSimSlotIndex();
399             try {
400                 countryCode = mTelephonyManager.getNetworkCountryIso(slotIndex);
401             } catch (IllegalArgumentException e) {
402                 Log.e(TAG, "Failed to get country code for slot index:" + slotIndex, e);
403                 continue;
404             }
405 
406             Log.d(TAG, "Telephony slot " + slotIndex + " country code is " + countryCode);
407             setTelephonyCountryCodeAndLastKnownCountryCode(
408                     slotIndex, countryCode, null /* lastKnownCountryCode */);
409         }
410     }
411 
setTelephonyCountryCodeAndLastKnownCountryCode( int slotIndex, String countryCode, String lastKnownCountryCode)412     private synchronized void setTelephonyCountryCodeAndLastKnownCountryCode(
413             int slotIndex, String countryCode, String lastKnownCountryCode) {
414         Log.d(
415                 TAG,
416                 "Set telephony country code to: "
417                         + countryCode
418                         + ", last country code to: "
419                         + lastKnownCountryCode
420                         + " for slotIndex: "
421                         + slotIndex);
422 
423         TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot =
424                 mTelephonyCountryCodeSlotInfoMap.computeIfAbsent(
425                         slotIndex, k -> new TelephonyCountryCodeSlotInfo());
426         telephonyCountryCodeInfoSlot.slotIndex = slotIndex;
427         telephonyCountryCodeInfoSlot.timestamp = Instant.now();
428         telephonyCountryCodeInfoSlot.countryCode = countryCode;
429         telephonyCountryCodeInfoSlot.lastKnownCountryCode = lastKnownCountryCode;
430 
431         mTelephonyCountryCodeInfo = null;
432         mTelephonyLastCountryCodeInfo = null;
433 
434         for (TelephonyCountryCodeSlotInfo slotInfo : mTelephonyCountryCodeSlotInfoMap.values()) {
435             if ((mTelephonyCountryCodeInfo == null) && isValidCountryCode(slotInfo.countryCode)) {
436                 mTelephonyCountryCodeInfo =
437                         new CountryCodeInfo(
438                                 slotInfo.countryCode,
439                                 COUNTRY_CODE_SOURCE_TELEPHONY,
440                                 slotInfo.timestamp);
441             }
442 
443             if ((mTelephonyLastCountryCodeInfo == null)
444                     && isValidCountryCode(slotInfo.lastKnownCountryCode)) {
445                 mTelephonyLastCountryCodeInfo =
446                         new CountryCodeInfo(
447                                 slotInfo.lastKnownCountryCode,
448                                 COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
449                                 slotInfo.timestamp);
450             }
451         }
452 
453         updateCountryCode(false /* forceUpdate */);
454     }
455 
456     /**
457      * Priority order of country code sources (we stop at the first known country code source):
458      *
459      * <ul>
460      *   <li>1. Override country code - Country code forced via shell command (local/automated
461      *       testing)
462      *   <li>2. Telephony country code - Current country code retrieved via cellular. If there are
463      *       multiple SIM's, the country code chosen is non-deterministic if they return different
464      *       codes. The first valid country code with the lowest slot number will be used.
465      *   <li>3. Wifi country code - Current country code retrieved via wifi (via 80211.ad).
466      *   <li>4. Last known telephony country code - Last known country code retrieved via cellular.
467      *       If there are multiple SIM's, the country code chosen is non-deterministic if they
468      *       return different codes. The first valid last known country code with the lowest slot
469      *       number will be used.
470      *   <li>5. Location country code - Country code retrieved from LocationManager passive location
471      *       provider.
472      *   <li>6. OEM country code - Country code retrieved from the system property
473      *       `threadnetwork.country_code`.
474      *   <li>7. Default country code `WW`.
475      * </ul>
476      *
477      * @return the selected country code information.
478      */
pickCountryCode()479     private CountryCodeInfo pickCountryCode() {
480         if (mOverrideCountryCodeInfo != null) {
481             return mOverrideCountryCodeInfo;
482         }
483 
484         if (mTelephonyCountryCodeInfo != null) {
485             return mTelephonyCountryCodeInfo;
486         }
487 
488         if (mWifiCountryCodeInfo != null) {
489             return mWifiCountryCodeInfo;
490         }
491 
492         if (mTelephonyLastCountryCodeInfo != null) {
493             return mTelephonyLastCountryCodeInfo;
494         }
495 
496         if (mLocationCountryCodeInfo != null) {
497             return mLocationCountryCodeInfo;
498         }
499 
500         String settingsCountryCode = mPersistentSettings.get(THREAD_COUNTRY_CODE);
501         if (settingsCountryCode != null) {
502             return new CountryCodeInfo(settingsCountryCode, COUNTRY_CODE_SOURCE_SETTINGS);
503         }
504 
505         if (mOemCountryCodeInfo != null) {
506             return mOemCountryCodeInfo;
507         }
508 
509         return DEFAULT_COUNTRY_CODE_INFO;
510     }
511 
newOperationReceiver(CountryCodeInfo countryCodeInfo)512     private IOperationReceiver newOperationReceiver(CountryCodeInfo countryCodeInfo) {
513         return new IOperationReceiver.Stub() {
514             @Override
515             public void onSuccess() {
516                 synchronized ("ThreadNetworkCountryCode.this") {
517                     mCurrentCountryCodeInfo = countryCodeInfo;
518                     mPersistentSettings.put(
519                             THREAD_COUNTRY_CODE.key, countryCodeInfo.getCountryCode());
520                 }
521             }
522 
523             @Override
524             public void onError(int otError, String message) {
525                 Log.e(
526                         TAG,
527                         "Error "
528                                 + otError
529                                 + ": "
530                                 + message
531                                 + ". Failed to set country code "
532                                 + countryCodeInfo);
533             }
534         };
535     }
536 
537     /**
538      * Updates country code to the Thread native layer.
539      *
540      * @param forceUpdate Force update the country code even if it was the same as previously cached
541      *     value.
542      */
543     @VisibleForTesting
544     public synchronized void updateCountryCode(boolean forceUpdate) {
545         CountryCodeInfo countryCodeInfo = pickCountryCode();
546 
547         if (!forceUpdate && countryCodeInfo.isCountryCodeMatch(mCurrentCountryCodeInfo)) {
548             Log.i(TAG, "Ignoring already set country code " + countryCodeInfo.getCountryCode());
549             return;
550         }
551 
552         Log.i(TAG, "Set country code: " + countryCodeInfo);
553         mThreadNetworkControllerService.setCountryCode(
554                 countryCodeInfo.getCountryCode().toUpperCase(Locale.ROOT),
555                 newOperationReceiver(countryCodeInfo));
556     }
557 
558     /** Returns the current country code. */
559     public synchronized String getCountryCode() {
560         return mCurrentCountryCodeInfo.getCountryCode();
561     }
562 
563     /**
564      * Returns {@code true} if {@code countryCode} is a valid country code.
565      *
566      * <p>A country code is valid if it consists of 2 alphabets.
567      */
568     public static boolean isValidCountryCode(String countryCode) {
569         return countryCode != null
570                 && countryCode.length() == 2
571                 && countryCode.chars().allMatch(Character::isLetter);
572     }
573 
574     /**
575      * Overrides any existing country code.
576      *
577      * @param countryCode A 2-Character alphabetical country code (as defined in ISO 3166).
578      * @throws IllegalArgumentException if {@code countryCode} is an invalid country code.
579      */
580     public synchronized void setOverrideCountryCode(String countryCode) {
581         if (!isValidCountryCode(countryCode)) {
582             throw new IllegalArgumentException("The override country code is invalid");
583         }
584 
585         mOverrideCountryCodeInfo = new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_OVERRIDE);
586         updateCountryCode(true /* forceUpdate */);
587     }
588 
589     /** Clears the country code previously set through {@link #setOverrideCountryCode} method. */
590     public synchronized void clearOverrideCountryCode() {
591         mOverrideCountryCodeInfo = null;
592         updateCountryCode(true /* forceUpdate */);
593     }
594 
595     /** Dumps the current state of this ThreadNetworkCountryCode object. */
596     public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
597         pw.println("---- Dump of ThreadNetworkCountryCode begin ----");
598         pw.println("mOverrideCountryCodeInfo        : " + mOverrideCountryCodeInfo);
599         pw.println("mTelephonyCountryCodeSlotInfoMap: " + mTelephonyCountryCodeSlotInfoMap);
600         pw.println("mTelephonyCountryCodeInfo       : " + mTelephonyCountryCodeInfo);
601         pw.println("mWifiCountryCodeInfo            : " + mWifiCountryCodeInfo);
602         pw.println("mTelephonyLastCountryCodeInfo   : " + mTelephonyLastCountryCodeInfo);
603         pw.println("mLocationCountryCodeInfo        : " + mLocationCountryCodeInfo);
604         pw.println("mOemCountryCodeInfo             : " + mOemCountryCodeInfo);
605         pw.println("mCurrentCountryCodeInfo         : " + mCurrentCountryCodeInfo);
606         pw.println("---- Dump of ThreadNetworkCountryCode end ------");
607     }
608 }
609