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