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.ArrayMap; 40 import android.util.ArraySet; 41 import android.util.Log; 42 import android.util.Pair; 43 44 import androidx.annotation.Nullable; 45 46 import com.android.internal.annotations.Keep; 47 import com.android.internal.annotations.VisibleForTesting; 48 import com.android.modules.utils.HandlerExecutor; 49 import com.android.server.uwb.jni.NativeUwbManager; 50 51 import java.io.FileDescriptor; 52 import java.io.PrintWriter; 53 import java.nio.charset.StandardCharsets; 54 import java.time.LocalDateTime; 55 import java.time.format.DateTimeFormatter; 56 import java.util.List; 57 import java.util.Locale; 58 import java.util.Map; 59 import java.util.Objects; 60 import java.util.Set; 61 import java.util.stream.Collectors; 62 63 /** 64 * Provide functions for making changes to UWB country code. 65 * This Country Code is from MCC or phone default setting. This class sends Country Code 66 * to UWB venodr via the HAL. 67 */ 68 public class UwbCountryCode { 69 private static final String TAG = "UwbCountryCode"; 70 // To be used when there is no country code available. 71 @VisibleForTesting 72 public static final String DEFAULT_COUNTRY_CODE = "00"; 73 private static final DateTimeFormatter FORMATTER = 74 DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); 75 /** 76 * Copied from {@link TelephonyManager} because it's @hide. 77 * TODO (b/242326831): Use @SystemApi. 78 */ 79 @VisibleForTesting 80 public static final String EXTRA_LAST_KNOWN_NETWORK_COUNTRY = 81 "android.telephony.extra.LAST_KNOWN_NETWORK_COUNTRY"; 82 83 // Wait 1 hour between updates 84 private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 1; 85 // Minimum distance before an update is triggered, in meters. We don't need this to be too 86 // exact because all we care about is what country the user is in. 87 private static final float DISTANCE_BETWEEN_UPDATES_METERS = 5_000.0f; 88 89 private final Context mContext; 90 private final Handler mHandler; 91 private final TelephonyManager mTelephonyManager; 92 private final SubscriptionManager mSubscriptionManager; 93 private final LocationManager mLocationManager; 94 private final Geocoder mGeocoder; 95 private final NativeUwbManager mNativeUwbManager; 96 private final UwbInjector mUwbInjector; 97 private final Set<CountryCodeChangedListener> mListeners = new ArraySet<>(); 98 99 private Map<Integer, TelephonyCountryCodeSlotInfo> mTelephonyCountryCodeInfoPerSlot = 100 new ArrayMap(); 101 private String mWifiCountryCode = null; 102 private String mLocationCountryCode = null; 103 private String mOverrideCountryCode = null; 104 private String mCountryCode = null; 105 private String mCountryCodeUpdatedTimestamp = null; 106 private String mWifiCountryTimestamp = null; 107 private String mLocationCountryTimestamp = null; 108 109 /** 110 * Container class to store country code per sim slot. 111 */ 112 public static class TelephonyCountryCodeSlotInfo { 113 public int slotIdx; 114 public String countryCode; 115 public String lastKnownCountryCode; 116 public String timestamp; 117 118 @Override toString()119 public String toString() { 120 return "TelephonyCountryCodeSlotInfo[ slotIdx: " + slotIdx 121 + ", countryCode: " + countryCode 122 + ", lastKnownCountryCode: " + lastKnownCountryCode 123 + ", timestamp: " + timestamp + "]"; 124 } 125 } 126 127 public interface CountryCodeChangedListener { onCountryCodeChanged(@ullable String newCountryCode)128 void onCountryCodeChanged(@Nullable String newCountryCode); 129 } 130 UwbCountryCode( Context context, NativeUwbManager nativeUwbManager, Handler handler, UwbInjector uwbInjector)131 public UwbCountryCode( 132 Context context, NativeUwbManager nativeUwbManager, Handler handler, 133 UwbInjector uwbInjector) { 134 mContext = context.createContext( 135 new ContextParams.Builder().setAttributionTag(TAG).build()); 136 mTelephonyManager = mContext.getSystemService(TelephonyManager.class); 137 mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); 138 mLocationManager = mContext.getSystemService(LocationManager.class); 139 mGeocoder = uwbInjector.makeGeocoder(); 140 mNativeUwbManager = nativeUwbManager; 141 mHandler = handler; 142 mUwbInjector = uwbInjector; 143 } 144 145 @Keep 146 private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback { onActiveCountryCodeChanged(@onNull String countryCode)147 public void onActiveCountryCodeChanged(@NonNull String countryCode) { 148 setWifiCountryCode(countryCode); 149 } 150 onCountryCodeInactive()151 public void onCountryCodeInactive() { 152 setWifiCountryCode(""); 153 } 154 } 155 setCountryCodeFromGeocodingLocation(@ullable Location location)156 private void setCountryCodeFromGeocodingLocation(@Nullable Location location) { 157 if (location == null) return; 158 Geocoder.GeocodeListener geocodeListener = (List<Address> addresses) -> { 159 if (addresses != null && !addresses.isEmpty()) { 160 String countryCode = addresses.get(0).getCountryCode(); 161 mHandler.post(() -> setLocationCountryCode(countryCode)); 162 } 163 }; 164 mGeocoder.getFromLocation( 165 location.getLatitude(), location.getLongitude(), 1, geocodeListener); 166 } 167 168 /** 169 * Initialize the module. 170 */ initialize()171 public void initialize() { 172 mContext.registerReceiver( 173 new BroadcastReceiver() { 174 @Override 175 public void onReceive(Context context, Intent intent) { 176 int slotIdx = intent.getIntExtra( 177 SubscriptionManager.EXTRA_SLOT_INDEX, 178 SubscriptionManager.INVALID_SIM_SLOT_INDEX); 179 String countryCode = intent.getStringExtra( 180 TelephonyManager.EXTRA_NETWORK_COUNTRY); 181 String lastKnownCountryCode = intent.getStringExtra( 182 EXTRA_LAST_KNOWN_NETWORK_COUNTRY); 183 Log.d(TAG, "Country code changed to: " + countryCode); 184 setTelephonyCountryCodeAndLastKnownCountryCode( 185 slotIdx, countryCode, lastKnownCountryCode); 186 } 187 }, 188 new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED), 189 null, mHandler); 190 if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) { 191 mContext.getSystemService(WifiManager.class).registerActiveCountryCodeChangedCallback( 192 new HandlerExecutor(mHandler), new WifiCountryCodeCallback()); 193 } 194 if (mUwbInjector.isGeocoderPresent()) { 195 mLocationManager.requestLocationUpdates( 196 LocationManager.PASSIVE_PROVIDER, 197 TIME_BETWEEN_UPDATES_MS, 198 DISTANCE_BETWEEN_UPDATES_METERS, 199 location -> setCountryCodeFromGeocodingLocation(location)); 200 201 } 202 Log.d(TAG, "Default country code from system property is " 203 + mUwbInjector.getOemDefaultCountryCode()); 204 List<SubscriptionInfo> subscriptionInfoList = 205 mSubscriptionManager.getActiveSubscriptionInfoList(); 206 if (subscriptionInfoList == null) return; // No sim 207 Set<Integer> slotIdxs = subscriptionInfoList 208 .stream() 209 .map(SubscriptionInfo::getSimSlotIndex) 210 .collect(Collectors.toSet()); 211 for (Integer slotIdx : slotIdxs) { 212 String countryCode; 213 try { 214 countryCode = mTelephonyManager.getNetworkCountryIso(slotIdx); 215 } catch (IllegalArgumentException e) { 216 Log.e(TAG, "Failed to get country code for slot id:" + slotIdx, e); 217 continue; 218 } 219 setTelephonyCountryCodeAndLastKnownCountryCode(slotIdx, countryCode, null); 220 } 221 if (mUwbInjector.isGeocoderPresent()) { 222 setCountryCodeFromGeocodingLocation( 223 mLocationManager.getLastKnownLocation(LocationManager.FUSED_PROVIDER)); 224 } 225 // Current Wifi country code update is sent immediately on registration. 226 } 227 addListener(@onNull CountryCodeChangedListener listener)228 public void addListener(@NonNull CountryCodeChangedListener listener) { 229 mListeners.add(listener); 230 } 231 setTelephonyCountryCodeAndLastKnownCountryCode(int slotIdx, String countryCode, String lastKnownCountryCode)232 private void setTelephonyCountryCodeAndLastKnownCountryCode(int slotIdx, String countryCode, 233 String lastKnownCountryCode) { 234 Log.d(TAG, "Set telephony country code to: " + countryCode 235 + ", last country code to: " + lastKnownCountryCode + " for slotIdx: " + slotIdx); 236 TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot = 237 mTelephonyCountryCodeInfoPerSlot.computeIfAbsent( 238 slotIdx, k -> new TelephonyCountryCodeSlotInfo()); 239 telephonyCountryCodeInfoSlot.slotIdx = slotIdx; 240 telephonyCountryCodeInfoSlot.timestamp = LocalDateTime.now().format(FORMATTER); 241 // Empty country code. 242 if (TextUtils.isEmpty(countryCode)) { 243 Log.d(TAG, "Received empty telephony country code"); 244 telephonyCountryCodeInfoSlot.countryCode = null; 245 } else { 246 telephonyCountryCodeInfoSlot.countryCode = countryCode.toUpperCase(Locale.US); 247 } 248 if (TextUtils.isEmpty(lastKnownCountryCode)) { 249 Log.d(TAG, "Received empty telephony last known country code"); 250 telephonyCountryCodeInfoSlot.lastKnownCountryCode = null; 251 } else { 252 telephonyCountryCodeInfoSlot.lastKnownCountryCode = 253 lastKnownCountryCode.toUpperCase(Locale.US); 254 } 255 setCountryCode(false); 256 } 257 setWifiCountryCode(String countryCode)258 private void setWifiCountryCode(String countryCode) { 259 Log.d(TAG, "Set wifi country code to: " + countryCode); 260 mWifiCountryTimestamp = LocalDateTime.now().format(FORMATTER); 261 // Empty country code. 262 if (TextUtils.isEmpty(countryCode) || TextUtils.equals(countryCode, DEFAULT_COUNTRY_CODE)) { 263 Log.d(TAG, "Received empty wifi country code"); 264 mWifiCountryCode = null; 265 } else { 266 mWifiCountryCode = countryCode.toUpperCase(Locale.US); 267 } 268 setCountryCode(false); 269 } 270 setLocationCountryCode(String countryCode)271 private void setLocationCountryCode(String countryCode) { 272 Log.d(TAG, "Set location country code to: " + countryCode); 273 mLocationCountryTimestamp = LocalDateTime.now().format(FORMATTER); 274 // Empty country code. 275 if (TextUtils.isEmpty(countryCode) || TextUtils.equals(countryCode, DEFAULT_COUNTRY_CODE)) { 276 Log.d(TAG, "Received empty location country code"); 277 mLocationCountryCode = null; 278 } else { 279 mLocationCountryCode = countryCode.toUpperCase(Locale.US); 280 } 281 setCountryCode(false); 282 } 283 284 /** 285 * Priority order of country code sources (we stop at the first known country code source): 286 * 1. Override country code - Country code forced via shell command (local/automated testing) 287 * 2. Telephony country code - Current country code retrieved via cellular. If there are 288 * multiple SIM's, the country code chosen is non-deterministic if they return different codes. 289 * 3. Wifi country code - Current country code retrieved via wifi (via 80211.ad). 290 * 4. Last known telephony country code - Last known country code retrieved via cellular. If 291 * there are multiple SIM's, the country code chosen is non-deterministic if they return 292 * different codes. 293 * 5. Location Country code - Country code retrieved from LocationManager Fused location 294 * provider. 295 * 6. OEM default country code - If set by the OEM, then we default to this country code. 296 * @return 297 */ pickCountryCode()298 private String pickCountryCode() { 299 if (mOverrideCountryCode != null) { 300 return mOverrideCountryCode; 301 } 302 for (TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot : 303 mTelephonyCountryCodeInfoPerSlot.values()) { 304 if (telephonyCountryCodeInfoSlot.countryCode != null) { 305 return telephonyCountryCodeInfoSlot.countryCode; 306 } 307 } 308 if (mWifiCountryCode != null) { 309 return mWifiCountryCode; 310 } 311 for (TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot : 312 mTelephonyCountryCodeInfoPerSlot.values()) { 313 if (telephonyCountryCodeInfoSlot.lastKnownCountryCode != null) { 314 return telephonyCountryCodeInfoSlot.lastKnownCountryCode; 315 } 316 } 317 if (mLocationCountryCode != null) { 318 return mLocationCountryCode; 319 } 320 return mUwbInjector.getOemDefaultCountryCode(); 321 } 322 323 /** 324 * Set country code 325 * 326 * @param forceUpdate Force update the country code even if it was the same as previously cached 327 * value. 328 * @return Pair<UWBS StatusCode from setting the country code, 329 * Country code that was attempted to be set in UWBS> 330 */ setCountryCode(boolean forceUpdate)331 public Pair<Integer, String> setCountryCode(boolean forceUpdate) { 332 String country = pickCountryCode(); 333 if (country == null) { 334 Log.i(TAG, "No valid country code, reset to " + DEFAULT_COUNTRY_CODE); 335 country = DEFAULT_COUNTRY_CODE; 336 } 337 if (!forceUpdate && Objects.equals(country, mCountryCode)) { 338 Log.i(TAG, "Ignoring already set country code: " + country); 339 return new Pair<>(STATUS_CODE_OK, mCountryCode); 340 } 341 Log.d(TAG, "setCountryCode to " + country); 342 int status = mNativeUwbManager.setCountryCode(country.getBytes(StandardCharsets.UTF_8)); 343 if (status != STATUS_CODE_OK) { 344 Log.i(TAG, "Failed to set country code, with status code: " + status); 345 return new Pair<>(status, country); 346 } 347 mCountryCode = country; 348 mCountryCodeUpdatedTimestamp = LocalDateTime.now().format(FORMATTER); 349 for (CountryCodeChangedListener listener : mListeners) { 350 listener.onCountryCodeChanged(country); 351 } 352 return new Pair<>(status, mCountryCode); 353 } 354 355 /** 356 * Get country code 357 * 358 * @return true if the country code is set successfully, false otherwise. 359 */ getCountryCode()360 public String getCountryCode() { 361 return mCountryCode; 362 } 363 364 /** 365 * Is this a valid country code 366 * @param countryCode A 2-Character alphanumeric country code. 367 * @return true if the countryCode is valid, false otherwise. 368 */ isValid(String countryCode)369 public static boolean isValid(String countryCode) { 370 return countryCode != null && countryCode.length() == 2 371 && countryCode.chars().allMatch(Character::isLetterOrDigit) 372 && !countryCode.equals(DEFAULT_COUNTRY_CODE); 373 } 374 375 /** 376 * This call will override any existing country code. 377 * This is for test purpose only and we should disallow any update from 378 * telephony in this mode. 379 * @param countryCode A 2-Character alphanumeric country code. 380 */ setOverrideCountryCode(String countryCode)381 public synchronized void setOverrideCountryCode(String countryCode) { 382 if (TextUtils.isEmpty(countryCode)) { 383 Log.d(TAG, "Fail to override country code because" 384 + "the received country code is empty"); 385 return; 386 } 387 mOverrideCountryCode = countryCode.toUpperCase(Locale.US); 388 setCountryCode(true); 389 } 390 391 /** 392 * This is for clearing the country code previously set through #setOverrideCountryCode() method 393 */ clearOverrideCountryCode()394 public synchronized void clearOverrideCountryCode() { 395 mOverrideCountryCode = null; 396 setCountryCode(true); 397 } 398 399 /** 400 * Method to dump the current state of this UwbCountryCode object. 401 */ dump(FileDescriptor fd, PrintWriter pw, String[] args)402 public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 403 pw.println("---- Dump of UwbCountryCode ----"); 404 pw.println("DefaultCountryCode(system property): " 405 + mUwbInjector.getOemDefaultCountryCode()); 406 pw.println("mOverrideCountryCode: " + mOverrideCountryCode); 407 pw.println("mTelephonyCountryCodeInfoSlot: " + mTelephonyCountryCodeInfoPerSlot); 408 pw.println("mWifiCountryCode: " + mWifiCountryCode); 409 pw.println("mWifiCountryTimestamp: " + mWifiCountryTimestamp); 410 pw.println("mLocationCountryCode: " + mLocationCountryCode); 411 pw.println("mLocationCountryTimestamp: " + mLocationCountryTimestamp); 412 pw.println("mCountryCode: " + mCountryCode); 413 pw.println("mCountryCodeUpdatedTimestamp: " + mCountryCodeUpdatedTimestamp); 414 pw.println("---- Dump of UwbCountryCode ----"); 415 } 416 } 417