• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.AlarmManager;
23 import android.app.PendingIntent;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.ContextParams;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.pm.PackageManager;
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.wifi.WifiManager;
36 import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
37 import android.os.Handler;
38 import android.provider.Settings;
39 import android.telephony.SubscriptionInfo;
40 import android.telephony.SubscriptionManager;
41 import android.telephony.TelephonyManager;
42 import android.text.TextUtils;
43 import android.util.ArraySet;
44 import android.util.Log;
45 import android.util.Pair;
46 
47 import androidx.annotation.Nullable;
48 
49 import com.android.internal.annotations.Keep;
50 import com.android.internal.annotations.VisibleForTesting;
51 import com.android.modules.utils.HandlerExecutor;
52 import com.android.server.uwb.jni.NativeUwbManager;
53 
54 import java.io.FileDescriptor;
55 import java.io.PrintWriter;
56 import java.nio.charset.StandardCharsets;
57 import java.time.LocalDateTime;
58 import java.time.format.DateTimeFormatter;
59 import java.util.ArrayList;
60 import java.util.List;
61 import java.util.Locale;
62 import java.util.Map;
63 import java.util.Objects;
64 import java.util.Optional;
65 import java.util.Set;
66 import java.util.concurrent.ConcurrentSkipListMap;
67 import java.util.stream.Collectors;
68 
69 /**
70  * Provide functions for making changes to UWB country code.
71  * This Country Code is from MCC or phone default setting. This class sends Country Code
72  * to UWB vendor via the HAL.
73  */
74 public class UwbCountryCode {
75     private static final String TAG = "UwbCountryCode";
76     // To be used when there is no country code available.
77     @VisibleForTesting
78     public static final String DEFAULT_COUNTRY_CODE = "00";
79     private static final DateTimeFormatter FORMATTER =
80             DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
81     /**
82      * Copied from {@link TelephonyManager} because it's @hide.
83      * TODO (b/242326831): Use @SystemApi.
84      */
85     @VisibleForTesting
86     public static final String EXTRA_LAST_KNOWN_NETWORK_COUNTRY =
87             "android.telephony.extra.LAST_KNOWN_NETWORK_COUNTRY";
88 
89     public static final String GEOCODER_RETRY_TIMEOUT_INTENT =
90             "com.android.uwb.uwbcountrycode.GEOCODE_RETRY";
91 
92     // Wait 1 hour between updates
93     private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 1;
94     // Minimum distance before an update is triggered, in meters. We don't need this to be too
95     // exact because all we care about is what country the user is in.
96     private static final float DISTANCE_BETWEEN_UPDATES_METERS = 5_000.0f;
97 
98     // The last SIM slot index, used when the slot is not known, so that the corresponding
99     // country code has the lowest priority (in the sorted mTelephonyCountryCodeInfoPerSlot map).
100     private static final int LAST_SIM_SLOT_INDEX = Integer.MAX_VALUE;
101 
102     // Wait between Fused updates
103     public static final long FUSED_TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 1;
104 
105     // Geocode Resolver timer timeout
106     public static final long GEOCODE_RESOLVER_FIRST_TIMEOUT_MS = 1000L * 5 * 1;
107     public static final long GEOCODE_RESOLVER_RETRY_TIMEOUT_MS = 1000L * 60 * 1;
108 
109     private final Context mContext;
110     private final Handler mHandler;
111     private final TelephonyManager mTelephonyManager;
112     private final SubscriptionManager mSubscriptionManager;
113     private final LocationManager mLocationManager;
114     private final Geocoder mGeocoder;
115     private final NativeUwbManager mNativeUwbManager;
116     private final UwbInjector mUwbInjector;
117     private final Set<CountryCodeChangedListener> mListeners = new ArraySet<>();
118 
119     private AlarmManager mGeocodeRetryTimer = null;
120     private Intent mRetryTimerIntent = new Intent(GEOCODER_RETRY_TIMEOUT_INTENT);
121     private BroadcastReceiver mRetryTimeoutReceiver;
122     private boolean mGeocoderRetryTimerActive = false;
123     private boolean mFusedLocationProviderActive = false;
124     private Map<Integer, TelephonyCountryCodeSlotInfo> mTelephonyCountryCodeInfoPerSlot =
125             new ConcurrentSkipListMap();
126     private String mWifiCountryCode = null;
127     private String mLocationCountryCode = null;
128     private String mCachedCountryCode = null;
129     private String mOverrideCountryCode = null;
130     private String mCountryCode = null;
131     private Optional<Integer> mCountryCodeStatus = Optional.empty();
132     private String mCountryCodeUpdatedTimestamp = null;
133     private String mWifiCountryTimestamp = null;
134     private String mLocationCountryTimestamp = null;
135     private boolean mIsMccMncOemOverrideEnabled = false;
136     private final List<MccMnc> mMccMncOemOverrideList = new ArrayList<>();
137 
138     /**
139      * Container class to store country code per sim slot.
140      */
141     public static class TelephonyCountryCodeSlotInfo {
142         public int slotIdx;
143         public String countryCode;
144         public String lastKnownCountryCode;
145         public String timestamp;
146 
147         @Override
toString()148         public String toString() {
149             return "TelephonyCountryCodeSlotInfo[ slotIdx: " + slotIdx
150                     + ", countryCode: " + countryCode
151                     + ", lastKnownCountryCode: " + lastKnownCountryCode
152                     + ", timestamp: " + timestamp + "]";
153         }
154     }
155 
156     public interface CountryCodeChangedListener {
157         /**
158          * Notify listeners about a country code change.
159          * @param statusCode - Status of the UWBS controller configuring the {@code newCountryCode}:
160          *         - STATUS_CODE_OK: The country code was successfully configured by UWBS.
161          *         - STATUS_CODE_ANDROID_REGULATION_UWB_OFF: UWB is not supported in the configured
162          *                   country code.
163          *         - Other status codes returned by the UWBS controller.
164          * @param newCountryCode - The new UWB country code configured in the UWBS controller.
165          */
onCountryCodeChanged(int statusCode, @Nullable String newCountryCode)166         void onCountryCodeChanged(int statusCode, @Nullable String newCountryCode);
167     }
168 
UwbCountryCode( Context context, NativeUwbManager nativeUwbManager, Handler handler, UwbInjector uwbInjector)169     public UwbCountryCode(
170             Context context, NativeUwbManager nativeUwbManager, Handler handler,
171             UwbInjector uwbInjector) {
172         mContext = context.createContext(
173                 new ContextParams.Builder().setAttributionTag(TAG).build());
174         mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
175         mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
176         mLocationManager = mContext.getSystemService(LocationManager.class);
177         mGeocoder = uwbInjector.makeGeocoder();
178         mNativeUwbManager = nativeUwbManager;
179         mHandler = handler;
180         mUwbInjector = uwbInjector;
181         mGeocodeRetryTimer = mContext.getSystemService(AlarmManager.class);
182     }
183 
184     @Keep
185     private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback {
onActiveCountryCodeChanged(@onNull String countryCode)186         public void onActiveCountryCodeChanged(@NonNull String countryCode) {
187             setWifiCountryCode(countryCode);
188         }
189 
onCountryCodeInactive()190         public void onCountryCodeInactive() {
191             setWifiCountryCode("");
192         }
193     }
194 
195     private class MccMnc {
196         private final int mMcc;
197         private final int mMnc;
MccMnc(int mcc, int mnc)198         MccMnc(int mcc, int mnc) {
199             mMcc = mcc;
200             mMnc = mnc;
201         }
getMcc()202         public int getMcc() {
203             return mMcc;
204         }
getMnc()205         public int getMnc() {
206             return mMnc;
207         }
208     }
209 
generateOemOverrideMccMncList()210     private void generateOemOverrideMccMncList() {
211         String[] mccMncOemOverrideList =
212             mUwbInjector.getDeviceConfigFacade().getMccMncOemOverrideList();
213         if (mccMncOemOverrideList == null) return;
214         for (String mccMnc : mccMncOemOverrideList) {
215             int mcc = -1;
216             int mnc = -1;
217             try {
218                 mcc = Integer.valueOf(mccMnc.substring(0, 3));
219             } catch (Exception e) {
220                 Log.e(TAG, "No mcc set", e);
221                 continue;
222             }
223             try {
224                 mnc = Integer.valueOf(mccMnc.substring(3));
225             } catch (Exception e) {
226                 Log.d(TAG, "No mnc set", e);
227             }
228             mMccMncOemOverrideList.add(new MccMnc(mcc, mnc));
229         }
230     }
231 
shouldOverrideCountryCodeForMccMncs()232     private boolean shouldOverrideCountryCodeForMccMncs() {
233         List<SubscriptionInfo> subscriptionInfoList =
234                 mSubscriptionManager.getCompleteActiveSubscriptionInfoList();
235         if (subscriptionInfoList != null && !subscriptionInfoList.isEmpty()) {
236             for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) {
237                 if (shouldOverrideCountryCodeForMccMnc(
238                         subscriptionInfo.getMccString(), subscriptionInfo.getMncString())) {
239                     return true;
240                 }
241             }
242         }
243         return false;
244     }
245 
shouldOverrideCountryCodeForMccMnc(String mccString, String mncString)246     private boolean shouldOverrideCountryCodeForMccMnc(String mccString, String mncString) {
247         if (TextUtils.isEmpty(mccString) || TextUtils.isEmpty(mncString)) return false;
248         try {
249             int mcc = Integer.valueOf(mccString);
250             int mnc = Integer.valueOf(mncString);
251             for (MccMnc mccMnc: mMccMncOemOverrideList) {
252                 if (mccMnc.getMcc() == mcc) {
253                     if (mccMnc.getMnc() == -1) {
254                         Log.i(TAG, "Override MCC meets " + mccMnc.getMcc());
255                         return true;
256                     } else if (mccMnc.getMnc() == mnc) {
257                         Log.i(TAG, "Override MCC MNC meets "
258                                 + mccMnc.getMcc() + ":" + mccMnc.getMnc());
259                         return true;
260                     }
261                 }
262             }
263         } catch (Exception e) {
264             Log.e(TAG, "failed in shouldOverrideCountryCodeForMccMnc", e);
265         }
266         return false;
267     }
268 
setCountryCodeFromGeocodingLocation(@ullable Location location)269     private void setCountryCodeFromGeocodingLocation(@Nullable Location location) {
270         if (location == null) return;
271         Geocoder.GeocodeListener geocodeListener = (List<Address> addresses) -> {
272             if (addresses != null && !addresses.isEmpty()) {
273                 String countryCode = addresses.get(0).getCountryCode();
274                 mHandler.post(() -> setLocationCountryCode(countryCode));
275             }
276         };
277         try {
278             mGeocoder.getFromLocation(
279                     location.getLatitude(), location.getLongitude(), 1, geocodeListener);
280         } catch (IllegalArgumentException e) {
281             // Wrong Type of Latitude and Longitude return from getFromLocation.
282             Log.e(TAG, "Exception while fetching latitude/longitude from location", e);
283 
284             // Call setCountryCode() to update country code from other sources.
285             mLocationCountryCode = null;
286             setCountryCode(false);
287         }
288     }
289 
290     /**
291      * Initialize the module.
292      */
initialize()293     public void initialize() {
294         // Read the cached country code first (if caching is enabled on the device)
295         if (mUwbInjector.getDeviceConfigFacade().isPersistentCacheUseForCountryCodeEnabled()) {
296             String cachedCountryCode = mUwbInjector.getUwbSettingsStore().get(
297                     UwbSettingsStore.SETTINGS_CACHED_COUNTRY_CODE);
298             if (isValid(cachedCountryCode)) mCachedCountryCode = cachedCountryCode;
299         }
300         IntentFilter filter = new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED);
301         generateOemOverrideMccMncList();
302         if (!mMccMncOemOverrideList.isEmpty()) {
303             filter.addAction(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED);
304         }
305         mContext.registerReceiver(
306                 new BroadcastReceiver() {
307                     @Override
308                     public void onReceive(Context context, Intent intent) {
309                         if (intent.getAction()
310                                 .equals(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)) {
311                             int slotIdx = intent.getIntExtra(
312                                     SubscriptionManager.EXTRA_SLOT_INDEX,
313                                     LAST_SIM_SLOT_INDEX);
314                             String countryCode = intent.getStringExtra(
315                                     TelephonyManager.EXTRA_NETWORK_COUNTRY);
316                             String lastKnownCountryCode = intent.getStringExtra(
317                                     EXTRA_LAST_KNOWN_NETWORK_COUNTRY);
318                             Log.d(TAG, "Telephony Country code changed to: " + countryCode);
319                             setTelephonyCountryCodeAndLastKnownCountryCode(
320                                     slotIdx, countryCode, lastKnownCountryCode);
321                         } else if (intent.getAction()
322                                 .equals(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED)) {
323                             if (!mMccMncOemOverrideList.isEmpty()) {
324                                 boolean shouldOverrideCountryCodeForMccMnc =
325                                         shouldOverrideCountryCodeForMccMncs();
326                                 if (mIsMccMncOemOverrideEnabled
327                                         != shouldOverrideCountryCodeForMccMnc) {
328                                     Log.i(TAG, "OEM override for mcc mnc changed");
329                                     mIsMccMncOemOverrideEnabled =
330                                             shouldOverrideCountryCodeForMccMnc;
331                                     setCountryCode(true);
332                                 }
333                             }
334                         }
335                     }
336                 },
337                 filter, null, mHandler);
338         try {
339             if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) {
340                 mContext.getSystemService(WifiManager.class)
341                         .registerActiveCountryCodeChangedCallback(
342                             new HandlerExecutor(mHandler), new WifiCountryCodeCallback());
343             }
344         } catch (SecurityException e) {
345             // failed to register WifiCountryCodeCallback
346             Log.e(TAG, "failed to register WifiCountryCodeCallback", e);
347             mWifiCountryCode = null;
348         }
349         if (mUwbInjector.getDeviceConfigFacade().isLocationUseForCountryCodeEnabled() &&
350                 mUwbInjector.isGeocoderPresent()) {
351             mLocationManager.requestLocationUpdates(
352                     LocationManager.PASSIVE_PROVIDER,
353                     TIME_BETWEEN_UPDATES_MS,
354                     DISTANCE_BETWEEN_UPDATES_METERS,
355                     location -> setCountryCodeFromGeocodingLocation(location));
356 
357         }
358         Log.d(TAG, "Default country code from system property is "
359                 + mUwbInjector.getOemDefaultCountryCode());
360         List<SubscriptionInfo> subscriptionInfoList =
361                 mSubscriptionManager.getActiveSubscriptionInfoList();
362         if (subscriptionInfoList != null && !subscriptionInfoList.isEmpty()) {
363             Set<Integer> slotIdxs = subscriptionInfoList
364                     .stream()
365                     .map(SubscriptionInfo::getSimSlotIndex)
366                     .collect(Collectors.toSet());
367             for (Integer slotIdx : slotIdxs) {
368                 String countryCode;
369                 try {
370                     countryCode = mTelephonyManager.getNetworkCountryIso(slotIdx);
371                 } catch (IllegalArgumentException e) {
372                     Log.e(TAG, "Failed to get country code for slot id:" + slotIdx, e);
373                     continue;
374                 }
375                 setTelephonyCountryCodeAndLastKnownCountryCode(slotIdx, countryCode, null);
376             }
377         } else {
378             // Fetch and configure the networkCountryIso() when the subscriptionInfoList is either
379             // null or empty. This is done only when the country code is valid.
380             String countryCode = mTelephonyManager.getNetworkCountryIso();
381             if (isValid(countryCode)) {
382                 setTelephonyCountryCodeAndLastKnownCountryCode(
383                         LAST_SIM_SLOT_INDEX, countryCode, null);
384             }
385         }
386         if (mUwbInjector.getDeviceConfigFacade().isLocationUseForCountryCodeEnabled() &&
387                 mUwbInjector.isGeocoderPresent()) {
388             setCountryCodeFromGeocodingLocation(
389                     mLocationManager.getLastKnownLocation(LocationManager.FUSED_PROVIDER));
390         }
391         if (mUwbInjector.getDeviceConfigFacade().isLocationUseForCountryCodeEnabled()
392                 && mUwbInjector.isGeocoderPresent() && !isValid(mCachedCountryCode)
393                 && mUwbInjector.getDeviceConfigFacade().isPersistentCacheUseForCountryCodeEnabled()
394                 && (mUwbInjector.getGlobalSettingsInt(
395                     Settings.Global.AIRPLANE_MODE_ON, 0) == 0)) {
396             startFusedLocationManager();
397         }
398         // Current Wifi country code update is sent immediately on registration.
399     }
400 
addListener(@onNull CountryCodeChangedListener listener)401     public void addListener(@NonNull CountryCodeChangedListener listener) {
402         mListeners.add(listener);
403     }
404 
405     /** Start Fused Provider Country Code Resolver */
startFusedLocationManager()406     private void startFusedLocationManager() {
407         if (mFusedLocationProviderActive || !mUwbInjector
408                 .getDeviceConfigFacade().isFusedCountryCodeProviderEnabled()) {
409             return;
410         }
411         Log.d(TAG, "Start Fused Country Code Resolver");
412         mLocationManager.requestLocationUpdates(LocationManager.FUSED_PROVIDER,
413                 FUSED_TIME_BETWEEN_UPDATES_MS, DISTANCE_BETWEEN_UPDATES_METERS,
414                 mFusedLocationListener, mUwbInjector.getUwbServiceLooper());
415         mFusedLocationProviderActive = true;
416     }
417 
418     /** Stop Fused Provider Country Code Resolver */
stopFusedLocationManager()419     private void stopFusedLocationManager() {
420         if (mFusedLocationProviderActive) {
421             Log.d(TAG, "Stopping Fused Country Code Resolver");
422             mLocationManager.removeUpdates(mFusedLocationListener);
423             mFusedLocationProviderActive = false;
424         }
425     }
426 
427     private final LocationListener mFusedLocationListener = new LocationListener() {
428         @Override
429         public void onLocationChanged(Location location) {
430             synchronized (UwbCountryCode.this) {
431                 Log.d(TAG, "Fused Provider onLocationChanged: " + location);
432                 if (location.isComplete()) {
433                     setCountryCodeFromGeocodingLocation(location);
434                     startRetryRequest();
435                     stopFusedLocationManager();
436                 }
437             }
438         }
439     };
440 
441     /** Start retry timer in case Geocode Resolver fails */
startRetryRequest()442     private void startRetryRequest() {
443         if (mGeocoderRetryTimerActive) return;
444 
445         Log.d(TAG, "Starting Geocode Resolver Timer");
446         mRetryTimeoutReceiver = new BroadcastReceiver() {
447             @Override public void onReceive(Context context, Intent intent) {
448                 Log.d(TAG, "Geocode Resolver Retry Timeout onReceive");
449                 setCountryCodeFromGeocodingLocation(
450                         mLocationManager.getLastKnownLocation(LocationManager.FUSED_PROVIDER));
451             }
452         };
453         mContext.registerReceiver(mRetryTimeoutReceiver,
454                 new IntentFilter(GEOCODER_RETRY_TIMEOUT_INTENT));
455         mGeocodeRetryTimer.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
456                 mUwbInjector.getElapsedSinceBootMillis() + GEOCODE_RESOLVER_FIRST_TIMEOUT_MS,
457                 GEOCODE_RESOLVER_RETRY_TIMEOUT_MS, getRetryTimerBroadcast());
458         mGeocoderRetryTimerActive = true;
459     }
460 
461     /** Stop retry timer in case Geocode Resolver fails */
stopRetryRequest()462     private void stopRetryRequest() {
463         if (mGeocoderRetryTimerActive) {
464             Log.d(TAG, "Stop Geocode Resolver timer");
465             mGeocodeRetryTimer.cancel(getRetryTimerBroadcast());
466             mContext.unregisterReceiver(mRetryTimeoutReceiver);
467             mGeocoderRetryTimerActive = false;
468         }
469     }
470 
getRetryTimerBroadcast()471     private PendingIntent getRetryTimerBroadcast() {
472         return PendingIntent.getBroadcast(mContext, 0, mRetryTimerIntent,
473                 PendingIntent.FLAG_IMMUTABLE);
474     }
475 
setTelephonyCountryCodeAndLastKnownCountryCode(int slotIdx, String countryCode, String lastKnownCountryCode)476     private void setTelephonyCountryCodeAndLastKnownCountryCode(int slotIdx, String countryCode,
477             String lastKnownCountryCode) {
478         Log.d(TAG, "Set telephony country code to: " + countryCode
479                 + ", last country code to: " + lastKnownCountryCode + " for slotIdx: " + slotIdx);
480         TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot =
481                 mTelephonyCountryCodeInfoPerSlot.computeIfAbsent(
482                         slotIdx, k -> new TelephonyCountryCodeSlotInfo());
483         telephonyCountryCodeInfoSlot.slotIdx = slotIdx;
484         telephonyCountryCodeInfoSlot.timestamp = LocalDateTime.now().format(FORMATTER);
485         // Empty country code.
486         if (!isValid(countryCode)) {
487             Log.d(TAG, "Received empty telephony country code");
488             telephonyCountryCodeInfoSlot.countryCode = null;
489         } else {
490             telephonyCountryCodeInfoSlot.countryCode = countryCode.toUpperCase(Locale.US);
491         }
492         if (!isValid(lastKnownCountryCode)) {
493             Log.d(TAG, "Received empty telephony last known country code");
494             telephonyCountryCodeInfoSlot.lastKnownCountryCode = null;
495         } else {
496             telephonyCountryCodeInfoSlot.lastKnownCountryCode =
497                     lastKnownCountryCode.toUpperCase(Locale.US);
498         }
499         setCountryCode(false);
500     }
501 
setWifiCountryCode(String countryCode)502     private void setWifiCountryCode(String countryCode) {
503         Log.d(TAG, "Set wifi country code to: " + countryCode);
504         mWifiCountryTimestamp = LocalDateTime.now().format(FORMATTER);
505         // Empty country code.
506         if (!isValid(countryCode)) {
507             Log.d(TAG, "Received empty wifi country code");
508             mWifiCountryCode = null;
509         } else {
510             mWifiCountryCode = countryCode.toUpperCase(Locale.US);
511         }
512         setCountryCode(false);
513     }
514 
setLocationCountryCode(String countryCode)515     private void setLocationCountryCode(String countryCode) {
516         Log.d(TAG, "Set location country code to: " + countryCode);
517         mLocationCountryTimestamp = LocalDateTime.now().format(FORMATTER);
518         // Empty country code.
519         if (!isValid(countryCode)) {
520             Log.d(TAG, "Received empty location country code");
521             mLocationCountryCode = null;
522         } else {
523             mLocationCountryCode = countryCode.toUpperCase(Locale.US);
524         }
525         setCountryCode(false);
526     }
527 
528     /**
529      * Priority order of country code sources (we stop at the first known country code source):
530      * 1. Override country code - Country code forced via shell command (local/automated testing)
531      * 2. Telephony country code - Current country code retrieved via cellular. If there are
532      * multiple SIM's, the country code chosen is non-deterministic if they return different codes.
533      * 3. Wifi country code - Current country code retrieved via wifi (via 80211.ad).
534      * 4. Last known telephony country code - Last known country code retrieved via cellular. If
535      * there are multiple SIM's, the country code chosen is non-deterministic if they return
536      * different codes.
537      * 5. Location Country code - If enabled, country code retrieved from LocationManager Fused
538      * location provider.
539      * 6. Cached Country code - If enabled (only enabled on non-phone form factors), uses last
540      * valid country code retrieved from any of the sources above (cache cleared on APM mode
541      * toggle).
542      * 7. OEM default country code - If set by the OEM, then we default to this country code.
543      * @return
544      */
pickCountryCode()545     private String pickCountryCode() {
546         if (mOverrideCountryCode != null) {
547             return mOverrideCountryCode;
548         }
549         if (mIsMccMncOemOverrideEnabled) {
550             return mUwbInjector.getOemDefaultCountryCode();
551         }
552         if (mTelephonyCountryCodeInfoPerSlot != null) {
553             for (TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot :
554                     mTelephonyCountryCodeInfoPerSlot.values()) {
555                 if (telephonyCountryCodeInfoSlot != null
556                         && telephonyCountryCodeInfoSlot.countryCode != null) {
557                     return telephonyCountryCodeInfoSlot.countryCode;
558                 }
559             }
560         }
561         if (mWifiCountryCode != null) {
562             return mWifiCountryCode;
563         }
564         if (mTelephonyCountryCodeInfoPerSlot != null) {
565             for (TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot :
566                     mTelephonyCountryCodeInfoPerSlot.values()) {
567                 if (telephonyCountryCodeInfoSlot != null
568                         && telephonyCountryCodeInfoSlot.lastKnownCountryCode != null) {
569                     return telephonyCountryCodeInfoSlot.lastKnownCountryCode;
570                 }
571             }
572         }
573         if (mLocationCountryCode != null) {
574             return mLocationCountryCode;
575         }
576         if (mUwbInjector.getDeviceConfigFacade().isPersistentCacheUseForCountryCodeEnabled()
577                 && mCachedCountryCode != null) {
578             Log.d(TAG, "Using cached country code");
579             return mCachedCountryCode;
580         }
581         return mUwbInjector.getOemDefaultCountryCode();
582     }
583 
584     /**
585      * Set country code
586      *
587      * @param forceUpdate Force update the country code even if it was the same as previously cached
588      *                    value.
589      * @return Pair<UWBS StatusCode from setting the country code,
590      *              Country code that was attempted to be set in UWBS>
591      */
setCountryCode(boolean forceUpdate)592     public Pair<Integer, String> setCountryCode(boolean forceUpdate) {
593         String country = pickCountryCode();
594         if (country == null) {
595             Log.i(TAG, "No valid country code, reset to " + DEFAULT_COUNTRY_CODE);
596             country = DEFAULT_COUNTRY_CODE;
597         }
598         if (isValid(country)) {
599             stopFusedLocationManager();
600             stopRetryRequest();
601         }
602         if (!forceUpdate && Objects.equals(country, mCountryCode)) {
603             Log.i(TAG, "Ignoring already set country code: " + country);
604             return new Pair<>(STATUS_CODE_OK, mCountryCode);
605         }
606         Log.d(TAG, "setCountryCode to " + country);
607         int status = mNativeUwbManager.setCountryCode(country.getBytes(StandardCharsets.UTF_8));
608         if (status != STATUS_CODE_OK) {
609             Log.i(TAG, "Failed to set country code, with status code: " + status);
610         }
611         mCountryCode = country;
612         mCountryCodeUpdatedTimestamp = LocalDateTime.now().format(FORMATTER);
613         mCountryCodeStatus = Optional.of(status);
614         // Cache the country code (if caching is enabled on the device)
615         if (mUwbInjector.getDeviceConfigFacade().isPersistentCacheUseForCountryCodeEnabled()
616                 && isValid(country)) {
617             mCachedCountryCode = country;
618             mUwbInjector.getUwbSettingsStore().put(
619                     UwbSettingsStore.SETTINGS_CACHED_COUNTRY_CODE, country);
620         }
621 
622         for (CountryCodeChangedListener listener : mListeners) {
623             listener.onCountryCodeChanged(status, country);
624         }
625         return new Pair<>(status, country);
626     }
627 
628     /**
629      * Get country code.
630      *
631      * @return the country code that was last configured in the UWBS.
632      */
getCountryCode()633     public String getCountryCode() {
634         return mCountryCode;
635     }
636 
637     /**
638      * Get country code configuration status.
639      *
640      * @return Status of the last attempt to configure a country code in the UWBS.
641      */
getCountryCodeStatus()642     public Optional<Integer> getCountryCodeStatus() {
643         return mCountryCodeStatus;
644     }
645 
646     /**
647      * Is this a valid country code
648      * @param countryCode A 2-Character alphanumeric country code.
649      * @return true if the countryCode is valid, false otherwise.
650      */
isValid(String countryCode)651     public static boolean isValid(String countryCode) {
652         return countryCode != null && countryCode.length() == 2
653                 && !countryCode.equals(DEFAULT_COUNTRY_CODE)
654                 && countryCode.chars().allMatch(Character::isLetter);
655     }
656 
657     /**
658      * This call will override any existing country code.
659      * This is for test purpose only and we should disallow any update from
660      * telephony in this mode.
661      * @param countryCode A 2-Character alphanumeric country code.
662      */
setOverrideCountryCode(String countryCode)663     public synchronized void setOverrideCountryCode(String countryCode) {
664         if (TextUtils.isEmpty(countryCode)) {
665             Log.d(TAG, "Fail to override country code because"
666                     + "the received country code is empty");
667             return;
668         }
669         mOverrideCountryCode = countryCode.toUpperCase(Locale.US);
670         setCountryCode(true);
671     }
672 
673     /**
674      * This is for clearing the country code previously set through #setOverrideCountryCode() method
675      */
clearOverrideCountryCode()676     public synchronized void clearOverrideCountryCode() {
677         mOverrideCountryCode = null;
678         setCountryCode(true);
679     }
680 
681     /**
682      * This is for clearing the cached country code when Airplane mode is toggled
683      */
clearCachedCountryCode()684     public synchronized void clearCachedCountryCode() {
685         if (mUwbInjector.getDeviceConfigFacade().isPersistentCacheUseForCountryCodeEnabled()) {
686             Log.d(TAG, "Clearing cached country code");
687             mCachedCountryCode = null;
688             mUwbInjector.getUwbSettingsStore().put(
689                     UwbSettingsStore.SETTINGS_CACHED_COUNTRY_CODE, "");
690         }
691         if (mUwbInjector.getGlobalSettingsInt(Settings.Global.AIRPLANE_MODE_ON, 0) == 1) {
692             stopFusedLocationManager();
693         } else {
694             startFusedLocationManager();
695         }
696     }
697 
698     /**
699      * Method to dump the current state of this UwbCountryCode object.
700      */
dump(FileDescriptor fd, PrintWriter pw, String[] args)701     public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
702         pw.println("---- Dump of UwbCountryCode ----");
703         pw.println("DefaultCountryCode(system property): "
704                 + mUwbInjector.getOemDefaultCountryCode());
705         pw.println("mOverrideCountryCode: " + mOverrideCountryCode);
706         pw.println("mTelephonyCountryCodeInfoSlot: " + mTelephonyCountryCodeInfoPerSlot);
707         pw.println("mWifiCountryCode: " + mWifiCountryCode);
708         pw.println("mWifiCountryTimestamp: " + mWifiCountryTimestamp);
709         pw.println("mLocationCountryCode: " + mLocationCountryCode);
710         pw.println("mLocationCountryTimestamp: " + mLocationCountryTimestamp);
711         pw.println("mCountryCode: " + mCountryCode);
712         pw.println("mCountryCodeStatus: "
713                 + (mCountryCodeStatus.isEmpty() ? "none" : mCountryCodeStatus.get()));
714         pw.println("mCountryCodeUpdatedTimestamp: " + mCountryCodeUpdatedTimestamp);
715         pw.println("mCachedCountryCode: " + mCachedCountryCode);
716         pw.println("---- Dump of UwbCountryCode ----");
717     }
718 }
719