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