• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.telephony;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.location.Address;
23 import android.location.Geocoder;
24 import android.location.Location;
25 import android.location.LocationListener;
26 import android.location.LocationManager;
27 import android.net.ConnectivityManager;
28 import android.net.Network;
29 import android.net.NetworkCapabilities;
30 import android.net.NetworkRequest;
31 import android.os.Build;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.os.HandlerThread;
35 import android.os.Looper;
36 import android.os.Message;
37 import android.os.RegistrantList;
38 import android.os.SystemClock;
39 import android.os.SystemProperties;
40 import android.telephony.Rlog;
41 import android.util.Pair;
42 
43 import com.android.internal.annotations.GuardedBy;
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.telephony.flags.FeatureFlags;
46 import com.android.internal.telephony.util.WorkerThread;
47 
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Locale;
52 import java.util.Map;
53 import java.util.concurrent.TimeUnit;
54 
55 /**
56  * This class is used to detect the country where the device is.
57  *
58  * {@link LocaleTracker} also tracks country of a device based on the information provided by
59  * network operators. However, it won't work when a device is out of service. In such cases and if
60  * Wi-Fi is available, {@link Geocoder} can be used to query the country for the current location of
61  * the device. {@link TelephonyCountryDetector} uses both {@link LocaleTracker} and {@link Geocoder}
62  * to track country of a device.
63  */
64 public class TelephonyCountryDetector extends Handler {
65     private static final String TAG = "TelephonyCountryDetector";
66     private static final String ALLOW_MOCK_MODEM_PROPERTY = "persist.radio.allow_mock_modem";
67     private static final String BOOT_ALLOW_MOCK_MODEM_PROPERTY = "ro.boot.radio.allow_mock_modem";
68     private static final boolean DEBUG = !"user".equals(Build.TYPE);
69     private static final int EVENT_LOCATION_CHANGED = 1;
70     private static final int EVENT_LOCATION_COUNTRY_CODE_CHANGED = 2;
71     private static final int EVENT_NETWORK_COUNTRY_CODE_CHANGED = 3;
72     private static final int EVENT_WIFI_CONNECTIVITY_STATE_CHANGED = 4;
73     private static final int EVENT_LOCATION_UPDATE_REQUEST_QUOTA_RESET = 5;
74 
75     // Wait 12 hours between location updates
76     private static final long TIME_BETWEEN_LOCATION_UPDATES_MILLIS = TimeUnit.HOURS.toMillis(12);
77     // Minimum distance before a location update is triggered, in meters. We don't need this to be
78     // too exact because all we care about is in what country the device is.
79     private static final float DISTANCE_BETWEEN_LOCATION_UPDATES_METERS = 2000;
80     protected static final long WAIT_FOR_LOCATION_UPDATE_REQUEST_QUOTA_RESET_TIMEOUT_MILLIS =
81             TimeUnit.MINUTES.toMillis(30);
82 
83     private static TelephonyCountryDetector sInstance;
84 
85     @NonNull private final Geocoder mGeocoder;
86     @NonNull private final LocationManager mLocationManager;
87     @NonNull private final ConnectivityManager mConnectivityManager;
88     @NonNull private final RegistrantList mWifiConnectivityStateChangedRegistrantList =
89             new RegistrantList();
90     @NonNull private final Object mLock = new Object();
91     @NonNull
92     @GuardedBy("mLock")
93     private final Map<Integer, NetworkCountryCodeInfo> mNetworkCountryCodeInfoPerPhone =
94             new HashMap<>();
95     @GuardedBy("mLock")
96     private String mLocationCountryCode = null;
97     /** This should be used by CTS only */
98     @GuardedBy("mLock")
99     private String mOverriddenLocationCountryCode = null;
100     @GuardedBy("mLock")
101     private boolean mIsLocationUpdateRequested = false;
102     @GuardedBy("mLock")
103     private long mLocationCountryCodeUpdatedTimestampNanos = 0;
104     /** This should be used by CTS only */
105     @GuardedBy("mLock")
106     private long mOverriddenLocationCountryCodeUpdatedTimestampNanos = 0;
107     @GuardedBy("mLock")
108     private List<String> mOverriddenCurrentNetworkCountryCodes = null;
109     @GuardedBy("mLock")
110     private Map<String, Long> mOverriddenCachedNetworkCountryCodes = new HashMap<>();
111     @GuardedBy("mLock")
112     private boolean mIsCountryCodesOverridden = false;
113     private final RegistrantList mCountryCodeChangedRegistrants = new RegistrantList();
114     private boolean mIsWifiNetworkConnected = false;
115 
116     private FeatureFlags mFeatureFlags = null;
117 
118     @NonNull private final LocationListener mLocationListener = new LocationListener() {
119         @Override
120         public void onLocationChanged(Location location) {
121             logd("onLocationChanged: " + (location != null));
122             if (location != null) {
123                 sendRequestAsync(EVENT_LOCATION_CHANGED, location);
124             }
125         }
126 
127         @Override
128         public void onProviderDisabled(String provider) {
129             logd("onProviderDisabled: provider=" + provider);
130         }
131 
132         @Override
133         public void onProviderEnabled(String provider) {
134             logd("onProviderEnabled: provider=" + provider);
135         }
136 
137         @Override
138         public void onStatusChanged(String provider, int status, Bundle extras) {
139             logd("onStatusChanged: provider=" + provider + ", status=" + status
140                     + ", extras=" + extras);
141         }
142     };
143 
144     private class TelephonyGeocodeListener implements Geocoder.GeocodeListener {
145         private long mLocationUpdatedTime;
TelephonyGeocodeListener(long locationUpdatedTime)146         TelephonyGeocodeListener(long locationUpdatedTime) {
147             mLocationUpdatedTime = locationUpdatedTime;
148         }
149 
150         @Override
onGeocode(List<Address> addresses)151         public void onGeocode(List<Address> addresses) {
152             if (addresses != null && !addresses.isEmpty()) {
153                 logd("onGeocode: addresses is available");
154                 String countryCode = addresses.get(0).getCountryCode();
155                 sendRequestAsync(EVENT_LOCATION_COUNTRY_CODE_CHANGED,
156                         new Pair<>(countryCode, mLocationUpdatedTime));
157             } else {
158                 logd("onGeocode: addresses is not available");
159             }
160         }
161 
162         @Override
onError(String errorMessage)163         public void onError(String errorMessage) {
164             loge("GeocodeListener.onError=" + errorMessage);
165         }
166     }
167 
168     /**
169      * Container class to store country code per Phone.
170      */
171     private static class NetworkCountryCodeInfo {
172         public int phoneId;
173         public String countryCode;
174         public long timestamp;
175 
176         @Override
toString()177         public String toString() {
178             return "NetworkCountryCodeInfo[phoneId: " + phoneId
179                     + ", countryCode: " + countryCode
180                     + ", timestamp: " + timestamp + "]";
181         }
182     }
183 
184     /**
185      * Create the singleton instance of {@link TelephonyCountryDetector}.
186      *
187      * @param looper The looper to run the {@link TelephonyCountryDetector} instance.
188      * @param context The context used by the instance.
189      * @param locationManager The LocationManager instance.
190      * @param connectivityManager The ConnectivityManager instance.
191      */
192     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
TelephonyCountryDetector(@onNull Looper looper, @NonNull Context context, @NonNull LocationManager locationManager, @NonNull ConnectivityManager connectivityManager, FeatureFlags featureFlags)193     protected TelephonyCountryDetector(@NonNull Looper looper, @NonNull Context context,
194             @NonNull LocationManager locationManager,
195             @NonNull ConnectivityManager connectivityManager,
196             FeatureFlags featureFlags) {
197         super(looper);
198         mLocationManager = locationManager;
199         mGeocoder = new Geocoder(context);
200         mConnectivityManager = connectivityManager;
201         mFeatureFlags = featureFlags;
202         initialize();
203     }
204 
205     /** @return the singleton instance of the {@link TelephonyCountryDetector} */
getInstance(@onNull Context context, FeatureFlags featureFlags)206     public static synchronized TelephonyCountryDetector getInstance(@NonNull Context context,
207             FeatureFlags featureFlags) {
208         if (sInstance == null) {
209             if (featureFlags.threadShred()) {
210                 sInstance = new TelephonyCountryDetector(WorkerThread.get().getLooper(), context,
211                         context.getSystemService(LocationManager.class),
212                         context.getSystemService(ConnectivityManager.class),
213                         featureFlags);
214             } else {
215                 HandlerThread handlerThread = new HandlerThread("TelephonyCountryDetector");
216                 handlerThread.start();
217                 sInstance = new TelephonyCountryDetector(handlerThread.getLooper(), context,
218                         context.getSystemService(LocationManager.class),
219                         context.getSystemService(ConnectivityManager.class),
220                         featureFlags);
221             }
222         }
223         return sInstance;
224     }
225 
226     /**
227      * @return The list of current network country ISOs if available, an empty list otherwise.
228      */
getCurrentNetworkCountryIso()229     @NonNull public List<String> getCurrentNetworkCountryIso() {
230         synchronized (mLock) {
231             if (mIsCountryCodesOverridden) {
232                 logd("mOverriddenCurrentNetworkCountryCodes="
233                         + String.join(", ", mOverriddenCurrentNetworkCountryCodes));
234                 return mOverriddenCurrentNetworkCountryCodes;
235             }
236         }
237 
238         List<String> result = new ArrayList<>();
239         for (Phone phone : PhoneFactory.getPhones()) {
240             String countryIso = getNetworkCountryIsoForPhone(phone);
241             if (isValid(countryIso)) {
242                 String countryIsoInUpperCase = countryIso.toUpperCase(Locale.US);
243                 if (!result.contains(countryIsoInUpperCase)) {
244                     result.add(countryIsoInUpperCase);
245                 }
246             } else {
247                 logd("getCurrentNetworkCountryIso: invalid countryIso=" + countryIso
248                         + " for phoneId=" + phone.getPhoneId() + ", subId=" + phone.getSubId());
249             }
250         }
251         return result;
252     }
253 
254     /**
255      * @return The cached location country code and its updated timestamp.
256      */
getCachedLocationCountryIsoInfo()257     @NonNull public Pair<String, Long> getCachedLocationCountryIsoInfo() {
258         synchronized (mLock) {
259             if (mIsCountryCodesOverridden) {
260                 logd("mOverriddenLocationCountryCode=" + mOverriddenLocationCountryCode
261                         + " will be used");
262                 return new Pair<>(mOverriddenLocationCountryCode,
263                         mOverriddenLocationCountryCodeUpdatedTimestampNanos);
264             }
265             return new Pair<>(mLocationCountryCode, mLocationCountryCodeUpdatedTimestampNanos);
266         }
267     }
268 
269     /**
270      * This API should be used only when {@link #getCurrentNetworkCountryIso()} returns an empty
271      * list.
272      *
273      * @return The list of cached network country codes and their updated timestamps.
274      */
getCachedNetworkCountryIsoInfo()275     @NonNull public Map<String, Long> getCachedNetworkCountryIsoInfo() {
276         synchronized (mLock) {
277             if (mIsCountryCodesOverridden) {
278                 logd("mOverriddenCachedNetworkCountryCodes = "
279                         + String.join(", ", mOverriddenCachedNetworkCountryCodes.keySet())
280                         + " will be used");
281                 return mOverriddenCachedNetworkCountryCodes;
282             }
283             Map<String, Long> result = new HashMap<>();
284             for (NetworkCountryCodeInfo countryCodeInfo :
285                     mNetworkCountryCodeInfoPerPhone.values()) {
286                 boolean alreadyAdded = result.containsKey(countryCodeInfo.countryCode);
287                 if (!alreadyAdded || (alreadyAdded
288                         && result.get(countryCodeInfo.countryCode) < countryCodeInfo.timestamp)) {
289                     result.put(countryCodeInfo.countryCode, countryCodeInfo.timestamp);
290                 }
291             }
292             return result;
293         }
294     }
295 
296     @Override
handleMessage(Message msg)297     public void handleMessage(Message msg) {
298         switch (msg.what) {
299             case EVENT_LOCATION_CHANGED:
300                 queryCountryCodeForLocation((Location) msg.obj);
301                 break;
302             case EVENT_LOCATION_COUNTRY_CODE_CHANGED:
303                 setLocationCountryCode((Pair) msg.obj);
304                 break;
305             case EVENT_NETWORK_COUNTRY_CODE_CHANGED:
306                 handleNetworkCountryCodeChangedEvent((NetworkCountryCodeInfo) msg.obj);
307                 break;
308             case EVENT_WIFI_CONNECTIVITY_STATE_CHANGED:
309                 handleEventWifiConnectivityStateChanged((boolean) msg.obj);
310                 break;
311             case EVENT_LOCATION_UPDATE_REQUEST_QUOTA_RESET:
312                 evaluateRequestingLocationUpdates();
313                 break;
314             default:
315                 logw("CountryDetectorHandler: unexpected message code: " + msg.what);
316                 break;
317         }
318     }
319 
320     /**
321      * This API is called by {@link LocaleTracker} whenever there is a change in network country
322      * code of a phone.
323      */
onNetworkCountryCodeChanged( @onNull Phone phone, @Nullable String currentCountryCode)324     public void onNetworkCountryCodeChanged(
325             @NonNull Phone phone, @Nullable String currentCountryCode) {
326         NetworkCountryCodeInfo networkCountryCodeInfo = new NetworkCountryCodeInfo();
327         networkCountryCodeInfo.phoneId = phone.getPhoneId();
328         networkCountryCodeInfo.countryCode = currentCountryCode;
329         sendRequestAsync(EVENT_NETWORK_COUNTRY_CODE_CHANGED, networkCountryCodeInfo);
330     }
331 
332     /**
333      * This API should be used by only CTS tests to forcefully set the telephony country codes.
334      */
setCountryCodes(boolean reset, @NonNull List<String> currentNetworkCountryCodes, @NonNull Map<String, Long> cachedNetworkCountryCodes, String locationCountryCode, long locationCountryCodeTimestampNanos)335     public boolean setCountryCodes(boolean reset, @NonNull List<String> currentNetworkCountryCodes,
336             @NonNull Map<String, Long> cachedNetworkCountryCodes, String locationCountryCode,
337             long locationCountryCodeTimestampNanos) {
338         if (!isMockModemAllowed()) {
339             logd("setCountryCodes: mock modem is not allowed");
340             return false;
341         }
342         logd("setCountryCodes: currentNetworkCountryCodes="
343                 + String.join(", ", currentNetworkCountryCodes)
344                 + ", locationCountryCode=" + locationCountryCode
345                 + ", locationCountryCodeTimestampNanos" + locationCountryCodeTimestampNanos
346                 + ", reset=" + reset + ", cachedNetworkCountryCodes="
347                 + String.join(", ", cachedNetworkCountryCodes.keySet()));
348 
349         synchronized (mLock) {
350             if (reset) {
351                 mIsCountryCodesOverridden = false;
352             } else {
353                 mIsCountryCodesOverridden = true;
354                 mOverriddenCachedNetworkCountryCodes = cachedNetworkCountryCodes;
355                 mOverriddenCurrentNetworkCountryCodes = currentNetworkCountryCodes;
356                 mOverriddenLocationCountryCode = locationCountryCode;
357                 mOverriddenLocationCountryCodeUpdatedTimestampNanos =
358                         locationCountryCodeTimestampNanos;
359             }
360         }
361         return true;
362     }
363 
364     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
queryCountryCodeForLocation(@onNull Location location)365     protected void queryCountryCodeForLocation(@NonNull Location location) {
366         mGeocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1,
367                 new TelephonyGeocodeListener(location.getElapsedRealtimeNanos()));
368     }
369 
370     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
getElapsedRealtimeNanos()371     protected long getElapsedRealtimeNanos() {
372         return SystemClock.elapsedRealtimeNanos();
373     }
374 
initialize()375     private void initialize() {
376         evaluateRequestingLocationUpdates();
377         registerForWifiConnectivityStateChanged();
378     }
379 
isGeoCoderImplemented()380     private boolean isGeoCoderImplemented() {
381         return Geocoder.isPresent();
382     }
383 
registerForLocationUpdates()384     private void registerForLocationUpdates() {
385         // If the device does not implement Geocoder, there is no point trying to get location
386         // updates because we cannot retrieve the country based on the location anyway.
387         if (!isGeoCoderImplemented()) {
388             logd("Geocoder is not implemented on the device");
389             return;
390         }
391 
392         synchronized (mLock) {
393             if (mIsLocationUpdateRequested) {
394                 logd("Already registered for location updates");
395                 return;
396             }
397 
398             logd("Registering for location updates");
399             /*
400              * PASSIVE_PROVIDER can be used to passively receive location updates when other
401              * applications or services request them without actually requesting the locations
402              * ourselves. This provider will only return locations generated by other providers.
403              * This provider is used to make sure there is no impact on the thermal and battery of
404              * a device.
405              */
406             mLocationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER,
407                     TIME_BETWEEN_LOCATION_UPDATES_MILLIS, DISTANCE_BETWEEN_LOCATION_UPDATES_METERS,
408                     mLocationListener);
409             mIsLocationUpdateRequested = true;
410             mLocationListener.onLocationChanged(getLastKnownLocation());
411         }
412     }
413 
414     @Nullable
getLastKnownLocation()415     private Location getLastKnownLocation() {
416         Location result = null;
417         for (String provider : mLocationManager.getProviders(true)) {
418             Location location = mLocationManager.getLastKnownLocation(provider);
419             if (location != null && (result == null
420                     || result.getElapsedRealtimeNanos() < location.getElapsedRealtimeNanos())) {
421                 result = location;
422             }
423         }
424         return result;
425     }
426 
unregisterForLocationUpdates()427     private void unregisterForLocationUpdates() {
428         synchronized (mLock) {
429             if (!mIsLocationUpdateRequested) {
430                 logd("Location update was not requested yet");
431                 return;
432             }
433             if (isLocationUpdateRequestQuotaExceeded()) {
434                 logd("Removing location updates will be re-evaluated after the quota is refilled");
435                 return;
436             }
437             mLocationManager.removeUpdates(mLocationListener);
438             mIsLocationUpdateRequested = false;
439             sendMessageDelayed(obtainMessage(EVENT_LOCATION_UPDATE_REQUEST_QUOTA_RESET),
440                     WAIT_FOR_LOCATION_UPDATE_REQUEST_QUOTA_RESET_TIMEOUT_MILLIS);
441         }
442     }
443 
isLocationUpdateRequestQuotaExceeded()444     private boolean isLocationUpdateRequestQuotaExceeded() {
445         return hasMessages(EVENT_LOCATION_UPDATE_REQUEST_QUOTA_RESET);
446     }
447 
shouldRequestLocationUpdate()448     private boolean shouldRequestLocationUpdate() {
449         return getCurrentNetworkCountryIso().isEmpty() && isWifiNetworkConnected();
450     }
451 
452     /**
453      * Posts the specified command to be executed on the main thread and returns immediately.
454      *
455      * @param command command to be executed on the main thread
456      * @param argument additional parameters required to perform of the operation
457      */
sendRequestAsync(int command, @NonNull Object argument)458     private void sendRequestAsync(int command, @NonNull Object argument) {
459         Message msg = this.obtainMessage(command, argument);
460         msg.sendToTarget();
461     }
462 
handleNetworkCountryCodeChangedEvent( @onNull NetworkCountryCodeInfo currentNetworkCountryCodeInfo)463     private void handleNetworkCountryCodeChangedEvent(
464             @NonNull NetworkCountryCodeInfo currentNetworkCountryCodeInfo) {
465         logd("currentNetworkCountryCodeInfo=" + currentNetworkCountryCodeInfo);
466         if (isValid(currentNetworkCountryCodeInfo.countryCode)) {
467             synchronized (mLock) {
468                 NetworkCountryCodeInfo cachedNetworkCountryCodeInfo =
469                         mNetworkCountryCodeInfoPerPhone.computeIfAbsent(
470                                 currentNetworkCountryCodeInfo.phoneId,
471                                 k -> new NetworkCountryCodeInfo());
472                 cachedNetworkCountryCodeInfo.phoneId = currentNetworkCountryCodeInfo.phoneId;
473                 cachedNetworkCountryCodeInfo.timestamp = getElapsedRealtimeNanos();
474                 cachedNetworkCountryCodeInfo.countryCode =
475                         currentNetworkCountryCodeInfo.countryCode.toUpperCase(Locale.US);
476             }
477         } else {
478             logd("handleNetworkCountryCodeChangedEvent: Got invalid or empty country code for "
479                     + "phoneId=" + currentNetworkCountryCodeInfo.phoneId);
480             synchronized (mLock) {
481                 if (mNetworkCountryCodeInfoPerPhone.containsKey(
482                         currentNetworkCountryCodeInfo.phoneId)) {
483                     // The country code has changed from valid to invalid. Thus, we need to update
484                     // the last valid timestamp.
485                     NetworkCountryCodeInfo cachedNetworkCountryCodeInfo =
486                             mNetworkCountryCodeInfoPerPhone.get(
487                                     currentNetworkCountryCodeInfo.phoneId);
488                     cachedNetworkCountryCodeInfo.timestamp = getElapsedRealtimeNanos();
489                 }
490             }
491         }
492         evaluateRequestingLocationUpdates();
493         logd("mCountryCodeChangedRegistrants.notifyRegistrants()");
494         mCountryCodeChangedRegistrants.notifyRegistrants();
495     }
496 
handleEventWifiConnectivityStateChanged(boolean connected)497     private void handleEventWifiConnectivityStateChanged(boolean connected) {
498         logd("handleEventWifiConnectivityStateChanged: " + connected);
499         evaluateNotifyWifiConnectivityStateChangedEvent(connected);
500         evaluateRequestingLocationUpdates();
501     }
502 
evaluateNotifyWifiConnectivityStateChangedEvent(boolean connected)503     private void evaluateNotifyWifiConnectivityStateChangedEvent(boolean connected) {
504         if (connected != mIsWifiNetworkConnected) {
505             mIsWifiNetworkConnected = connected;
506             mWifiConnectivityStateChangedRegistrantList.notifyResult(mIsWifiNetworkConnected);
507             logd("evaluateNotifyWifiConnectivityStateChangedEvent: wifi connectivity state has "
508                     + "changed to " + connected);
509         }
510     }
511 
setLocationCountryCode(@onNull Pair<String, Long> countryCodeInfo)512     private void setLocationCountryCode(@NonNull Pair<String, Long> countryCodeInfo) {
513         logd("Set location country code to: " + countryCodeInfo.first);
514         if (!isValid(countryCodeInfo.first)) {
515             logd("Received invalid location country code");
516         } else {
517             synchronized (mLock) {
518                 mLocationCountryCode = countryCodeInfo.first.toUpperCase(Locale.US);
519                 mLocationCountryCodeUpdatedTimestampNanos = countryCodeInfo.second;
520             }
521         }
522     }
523 
getNetworkCountryIsoForPhone(@onNull Phone phone)524     private String getNetworkCountryIsoForPhone(@NonNull Phone phone) {
525         ServiceStateTracker serviceStateTracker = phone.getServiceStateTracker();
526         if (serviceStateTracker == null) {
527             logw("getNetworkCountryIsoForPhone: serviceStateTracker is null");
528             return null;
529         }
530 
531         LocaleTracker localeTracker = serviceStateTracker.getLocaleTracker();
532         if (localeTracker == null) {
533             logw("getNetworkCountryIsoForPhone: localeTracker is null");
534             return null;
535         }
536 
537         return localeTracker.getCurrentCountry();
538     }
539 
registerForWifiConnectivityStateChanged()540     private void registerForWifiConnectivityStateChanged() {
541         logd("registerForWifiConnectivityStateChanged");
542         NetworkRequest.Builder builder = new NetworkRequest.Builder();
543         builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
544                 .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
545 
546         ConnectivityManager.NetworkCallback networkCallback =
547                 new ConnectivityManager.NetworkCallback() {
548                     @Override
549                     public void onCapabilitiesChanged(Network network,
550                             NetworkCapabilities networkCapabilities) {
551                         logd("onCapabilitiesChanged: " + networkCapabilities);
552                         sendRequestAsync(EVENT_WIFI_CONNECTIVITY_STATE_CHANGED,
553                                 isInternetAvailable(networkCapabilities));
554                     }
555 
556                     @Override
557                     public void onLost(Network network) {
558                         logd("Wifi network lost: " + network);
559                         sendRequestAsync(EVENT_WIFI_CONNECTIVITY_STATE_CHANGED, false);
560                     }
561                 };
562         mConnectivityManager.registerNetworkCallback(builder.build(), networkCallback);
563     }
564 
evaluateRequestingLocationUpdates()565     private void evaluateRequestingLocationUpdates() {
566         if (shouldRequestLocationUpdate()) {
567             registerForLocationUpdates();
568         } else {
569             unregisterForLocationUpdates();
570         }
571     }
572 
573     /**
574      * Check whether Wi-Fi network is connected or not.
575      * @return {@code true} is Wi-Fi is connected, and internet is available, {@code false}
576      * otherwise.
577      */
isWifiNetworkConnected()578     public boolean isWifiNetworkConnected() {
579         logd("isWifiNetworkConnected: " + mIsWifiNetworkConnected);
580         return mIsWifiNetworkConnected;
581     }
582 
isInternetAvailable(NetworkCapabilities networkCapabilities)583     private boolean isInternetAvailable(NetworkCapabilities networkCapabilities) {
584         boolean isWifiConnected =
585                 networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
586                 && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
587         logd("isWifiConnected: " + isWifiConnected);
588         return isWifiConnected;
589     }
590 
591     /**
592      * Register a callback to receive Wi-Fi connectivity state changes.
593      * @param h Handler for notification message
594      * @param what User-defined message code.
595      * @param obj User object.
596      */
registerForWifiConnectivityStateChanged(@onNull Handler h, int what, @Nullable Object obj)597     public void registerForWifiConnectivityStateChanged(@NonNull Handler h, int what,
598             @Nullable Object obj) {
599         mWifiConnectivityStateChangedRegistrantList.add(h, what, obj);
600     }
601 
602     /**
603      * Unregisters for Wi-Fi connectivity state changes.
604      * @param h Handler to be removed from the registrant list.
605      */
unregisterForWifiConnectivityStateChanged(@onNull Handler h)606     public void unregisterForWifiConnectivityStateChanged(@NonNull Handler h) {
607         mWifiConnectivityStateChangedRegistrantList.remove(h);
608     }
609 
610     /**
611      * Check whether this is a valid country code.
612      *
613      * @param countryCode A 2-Character alphanumeric country code.
614      * @return {@code true} if the countryCode is valid, {@code false} otherwise.
615      */
isValid(String countryCode)616     private static boolean isValid(String countryCode) {
617         return countryCode != null && countryCode.length() == 2
618                 && countryCode.chars().allMatch(Character::isLetterOrDigit);
619     }
620 
isMockModemAllowed()621     private static boolean isMockModemAllowed() {
622         return (DEBUG || SystemProperties.getBoolean(ALLOW_MOCK_MODEM_PROPERTY, false)
623                 || SystemProperties.getBoolean(BOOT_ALLOW_MOCK_MODEM_PROPERTY, false));
624     }
625 
626     /**
627      * Register a callback for country code changed events
628      *
629      * @param h    Handler to notify
630      * @param what msg.what when the message is delivered
631      * @param obj  AsyncResult.userObj when the message is delivered
632      */
registerForCountryCodeChanged(Handler h, int what, Object obj)633     public void registerForCountryCodeChanged(Handler h, int what, Object obj) {
634         mCountryCodeChangedRegistrants.add(h, what, obj);
635     }
636 
637     /**
638      * Unregister a callback for country code changed events
639      *
640      * @param h Handler to notifyf
641      */
unregisterForCountryCodeChanged(Handler h)642     public void unregisterForCountryCodeChanged(Handler h) {
643         mCountryCodeChangedRegistrants.remove(h);
644     }
645 
logd(@onNull String log)646     private static void logd(@NonNull String log) {
647         Rlog.d(TAG, log);
648     }
649 
logw(@onNull String log)650     private static void logw(@NonNull String log) {
651         Rlog.w(TAG, log);
652     }
653 
loge(@onNull String log)654     private static void loge(@NonNull String log) {
655         Rlog.e(TAG, log);
656     }
657 }
658