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