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