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