1 /* 2 * Copyright (C) 2021 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.uwb; 18 19 import static com.android.server.uwb.data.UwbUciConstants.STATUS_CODE_OK; 20 21 import android.annotation.NonNull; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.ContextParams; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.content.pm.PackageManager; 28 import android.location.Address; 29 import android.location.Geocoder; 30 import android.location.Location; 31 import android.location.LocationManager; 32 import android.net.wifi.WifiManager; 33 import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback; 34 import android.os.Handler; 35 import android.telephony.SubscriptionInfo; 36 import android.telephony.SubscriptionManager; 37 import android.telephony.TelephonyManager; 38 import android.text.TextUtils; 39 import android.util.ArraySet; 40 import android.util.Log; 41 import android.util.Pair; 42 43 import androidx.annotation.Nullable; 44 45 import com.android.internal.annotations.Keep; 46 import com.android.internal.annotations.VisibleForTesting; 47 import com.android.modules.utils.HandlerExecutor; 48 import com.android.server.uwb.jni.NativeUwbManager; 49 50 import java.io.FileDescriptor; 51 import java.io.PrintWriter; 52 import java.nio.charset.StandardCharsets; 53 import java.time.LocalDateTime; 54 import java.time.format.DateTimeFormatter; 55 import java.util.List; 56 import java.util.Locale; 57 import java.util.Map; 58 import java.util.Objects; 59 import java.util.Optional; 60 import java.util.Set; 61 import java.util.concurrent.ConcurrentSkipListMap; 62 import java.util.stream.Collectors; 63 64 /** 65 * Provide functions for making changes to UWB country code. 66 * This Country Code is from MCC or phone default setting. This class sends Country Code 67 * to UWB venodr via the HAL. 68 */ 69 public class UwbCountryCode { 70 private static final String TAG = "UwbCountryCode"; 71 // To be used when there is no country code available. 72 @VisibleForTesting 73 public static final String DEFAULT_COUNTRY_CODE = "00"; 74 private static final DateTimeFormatter FORMATTER = 75 DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); 76 /** 77 * Copied from {@link TelephonyManager} because it's @hide. 78 * TODO (b/242326831): Use @SystemApi. 79 */ 80 @VisibleForTesting 81 public static final String EXTRA_LAST_KNOWN_NETWORK_COUNTRY = 82 "android.telephony.extra.LAST_KNOWN_NETWORK_COUNTRY"; 83 84 // Wait 1 hour between updates 85 private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 1; 86 // Minimum distance before an update is triggered, in meters. We don't need this to be too 87 // exact because all we care about is what country the user is in. 88 private static final float DISTANCE_BETWEEN_UPDATES_METERS = 5_000.0f; 89 90 // The last SIM slot index, used when the slot is not known, so that the corresponding 91 // country code has the lowest priority (in the sorted mTelephonyCountryCodeInfoPerSlot map). 92 private static final int LAST_SIM_SLOT_INDEX = Integer.MAX_VALUE; 93 94 private final Context mContext; 95 private final Handler mHandler; 96 private final TelephonyManager mTelephonyManager; 97 private final SubscriptionManager mSubscriptionManager; 98 private final LocationManager mLocationManager; 99 private final Geocoder mGeocoder; 100 private final NativeUwbManager mNativeUwbManager; 101 private final UwbInjector mUwbInjector; 102 private final Set<CountryCodeChangedListener> mListeners = new ArraySet<>(); 103 104 private Map<Integer, TelephonyCountryCodeSlotInfo> mTelephonyCountryCodeInfoPerSlot = 105 new ConcurrentSkipListMap(); 106 private String mWifiCountryCode = null; 107 private String mLocationCountryCode = null; 108 private String mCachedCountryCode = null; 109 private String mOverrideCountryCode = null; 110 private String mCountryCode = null; 111 private Optional<Integer> mCountryCodeStatus = Optional.empty(); 112 private String mCountryCodeUpdatedTimestamp = null; 113 private String mWifiCountryTimestamp = null; 114 private String mLocationCountryTimestamp = null; 115 116 /** 117 * Container class to store country code per sim slot. 118 */ 119 public static class TelephonyCountryCodeSlotInfo { 120 public int slotIdx; 121 public String countryCode; 122 public String lastKnownCountryCode; 123 public String timestamp; 124 125 @Override toString()126 public String toString() { 127 return "TelephonyCountryCodeSlotInfo[ slotIdx: " + slotIdx 128 + ", countryCode: " + countryCode 129 + ", lastKnownCountryCode: " + lastKnownCountryCode 130 + ", timestamp: " + timestamp + "]"; 131 } 132 } 133 134 public interface CountryCodeChangedListener { 135 /** 136 * Notify listeners about a country code change. 137 * @param statusCode - Status of the UWBS controller configuring the {@code newCountryCode}: 138 * - STATUS_CODE_OK: The country code was successfully configured by UWBS. 139 * - STATUS_CODE_ANDROID_REGULATION_UWB_OFF: UWB is not supported in the configured 140 * country code. 141 * - Other status codes returned by the UWBS controller. 142 * @param newCountryCode - The new UWB country code configured in the UWBS controller. 143 */ onCountryCodeChanged(int statusCode, @Nullable String newCountryCode)144 void onCountryCodeChanged(int statusCode, @Nullable String newCountryCode); 145 } 146 UwbCountryCode( Context context, NativeUwbManager nativeUwbManager, Handler handler, UwbInjector uwbInjector)147 public UwbCountryCode( 148 Context context, NativeUwbManager nativeUwbManager, Handler handler, 149 UwbInjector uwbInjector) { 150 mContext = context.createContext( 151 new ContextParams.Builder().setAttributionTag(TAG).build()); 152 mTelephonyManager = mContext.getSystemService(TelephonyManager.class); 153 mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); 154 mLocationManager = mContext.getSystemService(LocationManager.class); 155 mGeocoder = uwbInjector.makeGeocoder(); 156 mNativeUwbManager = nativeUwbManager; 157 mHandler = handler; 158 mUwbInjector = uwbInjector; 159 } 160 161 @Keep 162 private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback { onActiveCountryCodeChanged(@onNull String countryCode)163 public void onActiveCountryCodeChanged(@NonNull String countryCode) { 164 setWifiCountryCode(countryCode); 165 } 166 onCountryCodeInactive()167 public void onCountryCodeInactive() { 168 setWifiCountryCode(""); 169 } 170 } 171 setCountryCodeFromGeocodingLocation(@ullable Location location)172 private void setCountryCodeFromGeocodingLocation(@Nullable Location location) { 173 if (location == null) return; 174 Geocoder.GeocodeListener geocodeListener = (List<Address> addresses) -> { 175 if (addresses != null && !addresses.isEmpty()) { 176 String countryCode = addresses.get(0).getCountryCode(); 177 mHandler.post(() -> setLocationCountryCode(countryCode)); 178 } 179 }; 180 try { 181 mGeocoder.getFromLocation( 182 location.getLatitude(), location.getLongitude(), 1, geocodeListener); 183 } catch (IllegalArgumentException e) { 184 // Wrong Type of Latitude and Longitude return from getFromLocation. 185 Log.e(TAG, "Exception while fetching latitude/longitude from location", e); 186 187 // Call setCountryCode() to update country code from other sources. 188 mLocationCountryCode = null; 189 setCountryCode(false); 190 } 191 } 192 193 /** 194 * Initialize the module. 195 */ initialize()196 public void initialize() { 197 // Read the cached country code first (if caching is enabled on the device) 198 if (mUwbInjector.getDeviceConfigFacade().isPersistentCacheUseForCountryCodeEnabled()) { 199 String cachedCountryCode = mUwbInjector.getUwbSettingsStore().get( 200 UwbSettingsStore.SETTINGS_CACHED_COUNTRY_CODE); 201 if (isValid(cachedCountryCode)) mCachedCountryCode = cachedCountryCode; 202 } 203 mContext.registerReceiver( 204 new BroadcastReceiver() { 205 @Override 206 public void onReceive(Context context, Intent intent) { 207 int slotIdx = intent.getIntExtra( 208 SubscriptionManager.EXTRA_SLOT_INDEX, 209 LAST_SIM_SLOT_INDEX); 210 String countryCode = intent.getStringExtra( 211 TelephonyManager.EXTRA_NETWORK_COUNTRY); 212 String lastKnownCountryCode = intent.getStringExtra( 213 EXTRA_LAST_KNOWN_NETWORK_COUNTRY); 214 Log.d(TAG, "Telephony Country code changed to: " + countryCode); 215 setTelephonyCountryCodeAndLastKnownCountryCode( 216 slotIdx, countryCode, lastKnownCountryCode); 217 } 218 }, 219 new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED), 220 null, mHandler); 221 try { 222 if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) { 223 mContext.getSystemService(WifiManager.class) 224 .registerActiveCountryCodeChangedCallback( 225 new HandlerExecutor(mHandler), new WifiCountryCodeCallback()); 226 } 227 } catch (SecurityException e) { 228 // failed to register WifiCountryCodeCallback 229 Log.e(TAG, "failed to register WifiCountryCodeCallback", e); 230 mWifiCountryCode = null; 231 } 232 if (mUwbInjector.getDeviceConfigFacade().isLocationUseForCountryCodeEnabled() && 233 mUwbInjector.isGeocoderPresent()) { 234 mLocationManager.requestLocationUpdates( 235 LocationManager.PASSIVE_PROVIDER, 236 TIME_BETWEEN_UPDATES_MS, 237 DISTANCE_BETWEEN_UPDATES_METERS, 238 location -> setCountryCodeFromGeocodingLocation(location)); 239 240 } 241 Log.d(TAG, "Default country code from system property is " 242 + mUwbInjector.getOemDefaultCountryCode()); 243 List<SubscriptionInfo> subscriptionInfoList = 244 mSubscriptionManager.getActiveSubscriptionInfoList(); 245 if (subscriptionInfoList != null && !subscriptionInfoList.isEmpty()) { 246 Set<Integer> slotIdxs = subscriptionInfoList 247 .stream() 248 .map(SubscriptionInfo::getSimSlotIndex) 249 .collect(Collectors.toSet()); 250 for (Integer slotIdx : slotIdxs) { 251 String countryCode; 252 try { 253 countryCode = mTelephonyManager.getNetworkCountryIso(slotIdx); 254 } catch (IllegalArgumentException e) { 255 Log.e(TAG, "Failed to get country code for slot id:" + slotIdx, e); 256 continue; 257 } 258 setTelephonyCountryCodeAndLastKnownCountryCode(slotIdx, countryCode, null); 259 } 260 } else { 261 // Fetch and configure the networkCountryIso() when the subscriptionInfoList is either 262 // null or empty. This is done only when the country code is valid. 263 String countryCode = mTelephonyManager.getNetworkCountryIso(); 264 if (isValid(countryCode)) { 265 setTelephonyCountryCodeAndLastKnownCountryCode( 266 LAST_SIM_SLOT_INDEX, countryCode, null); 267 } 268 } 269 270 if (mUwbInjector.getDeviceConfigFacade().isLocationUseForCountryCodeEnabled() && 271 mUwbInjector.isGeocoderPresent()) { 272 setCountryCodeFromGeocodingLocation( 273 mLocationManager.getLastKnownLocation(LocationManager.FUSED_PROVIDER)); 274 } 275 // Current Wifi country code update is sent immediately on registration. 276 } 277 addListener(@onNull CountryCodeChangedListener listener)278 public void addListener(@NonNull CountryCodeChangedListener listener) { 279 mListeners.add(listener); 280 } 281 setTelephonyCountryCodeAndLastKnownCountryCode(int slotIdx, String countryCode, String lastKnownCountryCode)282 private void setTelephonyCountryCodeAndLastKnownCountryCode(int slotIdx, String countryCode, 283 String lastKnownCountryCode) { 284 Log.d(TAG, "Set telephony country code to: " + countryCode 285 + ", last country code to: " + lastKnownCountryCode + " for slotIdx: " + slotIdx); 286 TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot = 287 mTelephonyCountryCodeInfoPerSlot.computeIfAbsent( 288 slotIdx, k -> new TelephonyCountryCodeSlotInfo()); 289 telephonyCountryCodeInfoSlot.slotIdx = slotIdx; 290 telephonyCountryCodeInfoSlot.timestamp = LocalDateTime.now().format(FORMATTER); 291 // Empty country code. 292 if (!isValid(countryCode)) { 293 Log.d(TAG, "Received empty telephony country code"); 294 telephonyCountryCodeInfoSlot.countryCode = null; 295 } else { 296 telephonyCountryCodeInfoSlot.countryCode = countryCode.toUpperCase(Locale.US); 297 } 298 if (!isValid(lastKnownCountryCode)) { 299 Log.d(TAG, "Received empty telephony last known country code"); 300 telephonyCountryCodeInfoSlot.lastKnownCountryCode = null; 301 } else { 302 telephonyCountryCodeInfoSlot.lastKnownCountryCode = 303 lastKnownCountryCode.toUpperCase(Locale.US); 304 } 305 setCountryCode(false); 306 } 307 setWifiCountryCode(String countryCode)308 private void setWifiCountryCode(String countryCode) { 309 Log.d(TAG, "Set wifi country code to: " + countryCode); 310 mWifiCountryTimestamp = LocalDateTime.now().format(FORMATTER); 311 // Empty country code. 312 if (!isValid(countryCode)) { 313 Log.d(TAG, "Received empty wifi country code"); 314 mWifiCountryCode = null; 315 } else { 316 mWifiCountryCode = countryCode.toUpperCase(Locale.US); 317 } 318 setCountryCode(false); 319 } 320 setLocationCountryCode(String countryCode)321 private void setLocationCountryCode(String countryCode) { 322 Log.d(TAG, "Set location country code to: " + countryCode); 323 mLocationCountryTimestamp = LocalDateTime.now().format(FORMATTER); 324 // Empty country code. 325 if (!isValid(countryCode)) { 326 Log.d(TAG, "Received empty location country code"); 327 mLocationCountryCode = null; 328 } else { 329 mLocationCountryCode = countryCode.toUpperCase(Locale.US); 330 } 331 setCountryCode(false); 332 } 333 334 /** 335 * Priority order of country code sources (we stop at the first known country code source): 336 * 1. Override country code - Country code forced via shell command (local/automated testing) 337 * 2. Telephony country code - Current country code retrieved via cellular. If there are 338 * multiple SIM's, the country code chosen is non-deterministic if they return different codes. 339 * 3. Wifi country code - Current country code retrieved via wifi (via 80211.ad). 340 * 4. Last known telephony country code - Last known country code retrieved via cellular. If 341 * there are multiple SIM's, the country code chosen is non-deterministic if they return 342 * different codes. 343 * 5. Location Country code - If enabled, country code retrieved from LocationManager Fused 344 * location provider. 345 * 6. Cached Country code - If enabled (only enabled on non-phone form factors), uses last 346 * valid country code retrieved from any of the sources above (cache cleared on APM mode 347 * toggle). 348 * 7. OEM default country code - If set by the OEM, then we default to this country code. 349 * @return 350 */ pickCountryCode()351 private String pickCountryCode() { 352 if (mOverrideCountryCode != null) { 353 return mOverrideCountryCode; 354 } 355 if (mTelephonyCountryCodeInfoPerSlot != null) { 356 for (TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot : 357 mTelephonyCountryCodeInfoPerSlot.values()) { 358 if (telephonyCountryCodeInfoSlot != null 359 && telephonyCountryCodeInfoSlot.countryCode != null) { 360 return telephonyCountryCodeInfoSlot.countryCode; 361 } 362 } 363 } 364 if (mWifiCountryCode != null) { 365 return mWifiCountryCode; 366 } 367 if (mTelephonyCountryCodeInfoPerSlot != null) { 368 for (TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot : 369 mTelephonyCountryCodeInfoPerSlot.values()) { 370 if (telephonyCountryCodeInfoSlot != null 371 && telephonyCountryCodeInfoSlot.lastKnownCountryCode != null) { 372 return telephonyCountryCodeInfoSlot.lastKnownCountryCode; 373 } 374 } 375 } 376 if (mLocationCountryCode != null) { 377 return mLocationCountryCode; 378 } 379 if (mUwbInjector.getDeviceConfigFacade().isPersistentCacheUseForCountryCodeEnabled() 380 && mCachedCountryCode != null) { 381 Log.d(TAG, "Using cached country code"); 382 return mCachedCountryCode; 383 } 384 return mUwbInjector.getOemDefaultCountryCode(); 385 } 386 387 /** 388 * Set country code 389 * 390 * @param forceUpdate Force update the country code even if it was the same as previously cached 391 * value. 392 * @return Pair<UWBS StatusCode from setting the country code, 393 * Country code that was attempted to be set in UWBS> 394 */ setCountryCode(boolean forceUpdate)395 public Pair<Integer, String> setCountryCode(boolean forceUpdate) { 396 String country = pickCountryCode(); 397 if (country == null) { 398 Log.i(TAG, "No valid country code, reset to " + DEFAULT_COUNTRY_CODE); 399 country = DEFAULT_COUNTRY_CODE; 400 } 401 if (!forceUpdate && Objects.equals(country, mCountryCode)) { 402 Log.i(TAG, "Ignoring already set country code: " + country); 403 return new Pair<>(STATUS_CODE_OK, mCountryCode); 404 } 405 Log.d(TAG, "setCountryCode to " + country); 406 int status = mNativeUwbManager.setCountryCode(country.getBytes(StandardCharsets.UTF_8)); 407 if (status != STATUS_CODE_OK) { 408 Log.i(TAG, "Failed to set country code, with status code: " + status); 409 } 410 mCountryCode = country; 411 mCountryCodeUpdatedTimestamp = LocalDateTime.now().format(FORMATTER); 412 mCountryCodeStatus = Optional.of(status); 413 // Cache the country code (if caching is enabled on the device) 414 if (mUwbInjector.getDeviceConfigFacade().isPersistentCacheUseForCountryCodeEnabled() 415 && isValid(country)) { 416 mCachedCountryCode = country; 417 mUwbInjector.getUwbSettingsStore().put( 418 UwbSettingsStore.SETTINGS_CACHED_COUNTRY_CODE, country); 419 } 420 421 for (CountryCodeChangedListener listener : mListeners) { 422 listener.onCountryCodeChanged(status, country); 423 } 424 return new Pair<>(status, country); 425 } 426 427 /** 428 * Get country code. 429 * 430 * @return the country code that was last configured in the UWBS. 431 */ getCountryCode()432 public String getCountryCode() { 433 return mCountryCode; 434 } 435 436 /** 437 * Get country code configuration status. 438 * 439 * @return Status of the last attempt to configure a country code in the UWBS. 440 */ getCountryCodeStatus()441 public Optional<Integer> getCountryCodeStatus() { 442 return mCountryCodeStatus; 443 } 444 445 /** 446 * Is this a valid country code 447 * @param countryCode A 2-Character alphanumeric country code. 448 * @return true if the countryCode is valid, false otherwise. 449 */ isValid(String countryCode)450 public static boolean isValid(String countryCode) { 451 return countryCode != null && countryCode.length() == 2 452 && !countryCode.equals(DEFAULT_COUNTRY_CODE) 453 && countryCode.chars().allMatch(Character::isLetter); 454 } 455 456 /** 457 * This call will override any existing country code. 458 * This is for test purpose only and we should disallow any update from 459 * telephony in this mode. 460 * @param countryCode A 2-Character alphanumeric country code. 461 */ setOverrideCountryCode(String countryCode)462 public synchronized void setOverrideCountryCode(String countryCode) { 463 if (TextUtils.isEmpty(countryCode)) { 464 Log.d(TAG, "Fail to override country code because" 465 + "the received country code is empty"); 466 return; 467 } 468 mOverrideCountryCode = countryCode.toUpperCase(Locale.US); 469 setCountryCode(true); 470 } 471 472 /** 473 * This is for clearing the country code previously set through #setOverrideCountryCode() method 474 */ clearOverrideCountryCode()475 public synchronized void clearOverrideCountryCode() { 476 mOverrideCountryCode = null; 477 setCountryCode(true); 478 } 479 480 /** 481 * This is for clearing the cached country code when Airplane mode is toggled 482 */ clearCachedCountryCode()483 public synchronized void clearCachedCountryCode() { 484 if (mUwbInjector.getDeviceConfigFacade().isPersistentCacheUseForCountryCodeEnabled()) { 485 Log.d(TAG, "Clearing cached country code"); 486 mCachedCountryCode = null; 487 mUwbInjector.getUwbSettingsStore().put( 488 UwbSettingsStore.SETTINGS_CACHED_COUNTRY_CODE, ""); 489 } 490 } 491 492 /** 493 * Method to dump the current state of this UwbCountryCode object. 494 */ dump(FileDescriptor fd, PrintWriter pw, String[] args)495 public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 496 pw.println("---- Dump of UwbCountryCode ----"); 497 pw.println("DefaultCountryCode(system property): " 498 + mUwbInjector.getOemDefaultCountryCode()); 499 pw.println("mOverrideCountryCode: " + mOverrideCountryCode); 500 pw.println("mTelephonyCountryCodeInfoSlot: " + mTelephonyCountryCodeInfoPerSlot); 501 pw.println("mWifiCountryCode: " + mWifiCountryCode); 502 pw.println("mWifiCountryTimestamp: " + mWifiCountryTimestamp); 503 pw.println("mLocationCountryCode: " + mLocationCountryCode); 504 pw.println("mLocationCountryTimestamp: " + mLocationCountryTimestamp); 505 pw.println("mCountryCode: " + mCountryCode); 506 pw.println("mCountryCodeStatus: " 507 + (mCountryCodeStatus.isEmpty() ? "none" : mCountryCodeStatus.get())); 508 pw.println("mCountryCodeUpdatedTimestamp: " + mCountryCodeUpdatedTimestamp); 509 pw.println("mCachedCountryCode: " + mCachedCountryCode); 510 pw.println("---- Dump of UwbCountryCode ----"); 511 } 512 } 513