1 /* 2 * Copyright (C) 2017 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 package com.android.car.settings.wifi; 17 18 import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLED; 19 import static android.os.UserManager.DISALLOW_CONFIG_WIFI; 20 21 import static com.android.car.settings.common.PreferenceController.AVAILABLE; 22 import static com.android.car.settings.common.PreferenceController.AVAILABLE_FOR_VIEWING; 23 import static com.android.car.settings.common.PreferenceController.UNSUPPORTED_ON_DEVICE; 24 import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG; 25 import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByDpm; 26 import static com.android.car.settings.enterprise.EnterpriseUtils.hasUserRestrictionByUm; 27 28 import android.annotation.DrawableRes; 29 import android.annotation.Nullable; 30 import android.app.admin.DevicePolicyManager; 31 import android.content.ComponentName; 32 import android.content.ContentResolver; 33 import android.content.Context; 34 import android.content.pm.PackageManager; 35 import android.net.ConnectivityManager; 36 import android.net.NetworkCapabilities; 37 import android.net.wifi.ScanResult; 38 import android.net.wifi.WifiConfiguration; 39 import android.net.wifi.WifiManager; 40 import android.os.Handler; 41 import android.os.SimpleClock; 42 import android.provider.Settings; 43 import android.text.TextUtils; 44 import android.widget.Toast; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.StringRes; 48 import androidx.lifecycle.Lifecycle; 49 50 import com.android.car.settings.R; 51 import com.android.car.settings.common.FragmentController; 52 import com.android.car.settings.common.Logger; 53 import com.android.car.settings.enterprise.EnterpriseUtils; 54 import com.android.wifitrackerlib.NetworkDetailsTracker; 55 import com.android.wifitrackerlib.WifiEntry; 56 import com.android.wifitrackerlib.WifiPickerTracker; 57 58 import java.time.Clock; 59 import java.time.ZoneOffset; 60 import java.util.regex.Pattern; 61 62 /** 63 * A collections of util functions for WIFI. 64 */ 65 public class WifiUtil { 66 67 private static final Logger LOG = new Logger(WifiUtil.class); 68 69 /** Value that is returned when we fail to connect wifi. */ 70 public static final int INVALID_NET_ID = -1; 71 /** Max age of tracked WifiEntries. */ 72 private static final long DEFAULT_MAX_SCAN_AGE_MILLIS = 15_000; 73 /** Interval between initiating WifiPickerTracker scans. */ 74 private static final long DEFAULT_SCAN_INTERVAL_MILLIS = 10_000; 75 private static final Pattern HEX_PATTERN = Pattern.compile("^[0-9A-F]+$"); 76 77 /** Clock used for evaluating the age of WiFi scans */ 78 private static final Clock ELAPSED_REALTIME_CLOCK = new SimpleClock(ZoneOffset.UTC) { 79 @Override 80 public long millis() { 81 return android.os.SystemClock.elapsedRealtime(); 82 } 83 }; 84 85 @DrawableRes getIconRes(int state)86 public static int getIconRes(int state) { 87 switch (state) { 88 case WifiManager.WIFI_STATE_ENABLING: 89 case WifiManager.WIFI_STATE_DISABLED: 90 return R.drawable.ic_settings_wifi_disabled; 91 default: 92 return R.drawable.ic_settings_wifi; 93 } 94 } 95 isWifiOn(int state)96 public static boolean isWifiOn(int state) { 97 switch (state) { 98 case WifiManager.WIFI_STATE_ENABLING: 99 case WifiManager.WIFI_STATE_DISABLED: 100 return false; 101 default: 102 return true; 103 } 104 } 105 106 /** 107 * @return 0 if no proper description can be found. 108 */ 109 @StringRes getStateDesc(int state)110 public static Integer getStateDesc(int state) { 111 switch (state) { 112 case WifiManager.WIFI_STATE_ENABLING: 113 return R.string.wifi_starting; 114 case WifiManager.WIFI_STATE_DISABLING: 115 return R.string.wifi_stopping; 116 case WifiManager.WIFI_STATE_DISABLED: 117 return R.string.wifi_disabled; 118 default: 119 return 0; 120 } 121 } 122 123 /** 124 * Returns {@code true} if wifi is available on this device. 125 */ isWifiAvailable(Context context)126 public static boolean isWifiAvailable(Context context) { 127 return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI); 128 } 129 130 /** 131 * Returns {@code true} if configuring wifi is allowed by user manager. 132 */ isConfigWifiRestrictedByUm(Context context)133 public static boolean isConfigWifiRestrictedByUm(Context context) { 134 return hasUserRestrictionByUm(context, DISALLOW_CONFIG_WIFI); 135 } 136 137 /** 138 * Returns {@code true} if configuring wifi is allowed by device policy manager. 139 */ isConfigWifiRestrictedByDpm(Context context)140 public static boolean isConfigWifiRestrictedByDpm(Context context) { 141 return hasUserRestrictionByDpm(context, DISALLOW_CONFIG_WIFI); 142 } 143 144 /** 145 * Returns Preference's availability status. 146 */ getAvailabilityStatus(Context context)147 public static int getAvailabilityStatus(Context context) { 148 if (!isWifiAvailable(context)) { 149 return UNSUPPORTED_ON_DEVICE; 150 } 151 if (isConfigWifiRestrictedByUm(context) 152 || isConfigWifiRestrictedByDpm(context)) { 153 return AVAILABLE_FOR_VIEWING; 154 } 155 return AVAILABLE; 156 } 157 158 /** 159 * Gets a unique key for a {@link WifiEntry}. 160 */ getKey(WifiEntry wifiEntry)161 public static String getKey(WifiEntry wifiEntry) { 162 return String.valueOf(wifiEntry.hashCode()); 163 } 164 165 /** 166 * This method is a stripped and negated version of WifiConfigStore.canModifyNetwork. 167 * 168 * @param context Context of caller 169 * @param config The WiFi config. 170 * @return {@code true} if Settings cannot modify the config due to lockDown. 171 */ isNetworkLockedDown(Context context, WifiConfiguration config)172 public static boolean isNetworkLockedDown(Context context, WifiConfiguration config) { 173 if (config == null) { 174 return false; 175 } 176 177 final DevicePolicyManager dpm = 178 (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); 179 final PackageManager pm = context.getPackageManager(); 180 181 // Check if device has DPM capability. If it has and dpm is still null, then we 182 // treat this case with suspicion and bail out. 183 if (pm.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN) && dpm == null) { 184 return true; 185 } 186 187 boolean isConfigEligibleForLockdown = false; 188 if (dpm != null) { 189 final ComponentName deviceOwner = dpm.getDeviceOwnerComponentOnAnyUser(); 190 if (deviceOwner != null) { 191 final int deviceOwnerUserId = dpm.getDeviceOwnerUserId(); 192 try { 193 final int deviceOwnerUid = pm.getPackageUidAsUser(deviceOwner.getPackageName(), 194 deviceOwnerUserId); 195 isConfigEligibleForLockdown = deviceOwnerUid == config.creatorUid; 196 } catch (PackageManager.NameNotFoundException e) { 197 // don't care 198 } 199 } 200 } 201 if (!isConfigEligibleForLockdown) { 202 return false; 203 } 204 205 final ContentResolver resolver = context.getContentResolver(); 206 final boolean isLockdownFeatureEnabled = Settings.Global.getInt(resolver, 207 Settings.Global.WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN, 0) != 0; 208 return isLockdownFeatureEnabled; 209 } 210 211 /** 212 * Returns {@code true} if the network security type doesn't require authentication. 213 */ isOpenNetwork(int security)214 public static boolean isOpenNetwork(int security) { 215 return security == WifiEntry.SECURITY_NONE || security == WifiEntry.SECURITY_OWE; 216 } 217 218 /** 219 * Returns {@code true} if the provided NetworkCapabilities indicate a captive portal network. 220 */ canSignIntoNetwork(NetworkCapabilities capabilities)221 public static boolean canSignIntoNetwork(NetworkCapabilities capabilities) { 222 return (capabilities != null 223 && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL)); 224 } 225 226 /** 227 * Attempts to connect to a specified Wi-Fi entry. 228 * 229 * @param listener for callbacks on success or failure of connection attempt (can be null) 230 */ connectToWifiEntry(Context context, String ssid, int security, String password, boolean hidden, @Nullable WifiManager.ActionListener listener)231 public static void connectToWifiEntry(Context context, String ssid, int security, 232 String password, boolean hidden, @Nullable WifiManager.ActionListener listener) { 233 WifiManager wifiManager = context.getSystemService(WifiManager.class); 234 WifiConfiguration wifiConfig = getWifiConfig(ssid, security, password, hidden); 235 wifiManager.connect(wifiConfig, listener); 236 } 237 getWifiConfig(String ssid, int security, String password, boolean hidden)238 private static WifiConfiguration getWifiConfig(String ssid, int security, 239 String password, boolean hidden) { 240 WifiConfiguration wifiConfig = new WifiConfiguration(); 241 wifiConfig.SSID = String.format("\"%s\"", ssid); 242 wifiConfig.hiddenSSID = hidden; 243 244 return finishWifiConfig(wifiConfig, security, password); 245 } 246 247 /** Similar to above, but uses WifiEntry to get additional relevant information. */ getWifiConfig(@onNull WifiEntry wifiEntry, String password)248 public static WifiConfiguration getWifiConfig(@NonNull WifiEntry wifiEntry, 249 String password) { 250 WifiConfiguration wifiConfig = new WifiConfiguration(); 251 if (wifiEntry.getWifiConfiguration() == null) { 252 wifiConfig.SSID = "\"" + wifiEntry.getSsid() + "\""; 253 } else { 254 wifiConfig.networkId = wifiEntry.getWifiConfiguration().networkId; 255 wifiConfig.hiddenSSID = wifiEntry.getWifiConfiguration().hiddenSSID; 256 } 257 258 return finishWifiConfig(wifiConfig, wifiEntry.getSecurity(), password); 259 } 260 finishWifiConfig(WifiConfiguration wifiConfig, int security, String password)261 private static WifiConfiguration finishWifiConfig(WifiConfiguration wifiConfig, int security, 262 String password) { 263 switch (security) { 264 case WifiEntry.SECURITY_NONE: 265 wifiConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OPEN); 266 break; 267 case WifiEntry.SECURITY_WEP: 268 wifiConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_WEP); 269 if (!TextUtils.isEmpty(password)) { 270 int length = password.length(); 271 // WEP-40, WEP-104, and 256-bit WEP (WEP-232?) 272 if ((length == 10 || length == 26 || length == 58) 273 && password.matches("[0-9A-Fa-f]*")) { 274 wifiConfig.wepKeys[0] = password; 275 } else { 276 wifiConfig.wepKeys[0] = '"' + password + '"'; 277 } 278 } 279 break; 280 case WifiEntry.SECURITY_PSK: 281 wifiConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_PSK); 282 if (!TextUtils.isEmpty(password)) { 283 if (password.matches("[0-9A-Fa-f]{64}")) { 284 wifiConfig.preSharedKey = password; 285 } else { 286 wifiConfig.preSharedKey = '"' + password + '"'; 287 } 288 } 289 break; 290 case WifiEntry.SECURITY_EAP: 291 case WifiEntry.SECURITY_EAP_SUITE_B: 292 if (security == WifiEntry.SECURITY_EAP_SUITE_B) { 293 // allowedSuiteBCiphers will be set according to certificate type 294 wifiConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_EAP_SUITE_B); 295 } else { 296 wifiConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_EAP); 297 } 298 if (!TextUtils.isEmpty(password)) { 299 wifiConfig.enterpriseConfig.setPassword(password); 300 } 301 break; 302 case WifiEntry.SECURITY_SAE: 303 wifiConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_SAE); 304 if (!TextUtils.isEmpty(password)) { 305 wifiConfig.preSharedKey = '"' + password + '"'; 306 } 307 break; 308 case WifiEntry.SECURITY_OWE: 309 wifiConfig.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OWE); 310 break; 311 default: 312 throw new IllegalArgumentException("unknown security type " + security); 313 } 314 return wifiConfig; 315 } 316 317 /** Returns {@code true} if the Wi-Fi entry is connected or connecting. */ isWifiEntryConnectedOrConnecting(WifiEntry wifiEntry)318 public static boolean isWifiEntryConnectedOrConnecting(WifiEntry wifiEntry) { 319 if (wifiEntry == null) { 320 return false; 321 } 322 return wifiEntry.getConnectedState() != WifiEntry.CONNECTED_STATE_DISCONNECTED; 323 } 324 325 /** Returns {@code true} if the Wi-Fi entry was disabled due to the wrong password. */ isWifiEntryDisabledByWrongPassword(WifiEntry wifiEntry)326 public static boolean isWifiEntryDisabledByWrongPassword(WifiEntry wifiEntry) { 327 WifiConfiguration config = wifiEntry.getWifiConfiguration(); 328 if (config == null) { 329 return false; 330 } 331 WifiConfiguration.NetworkSelectionStatus networkStatus = 332 config.getNetworkSelectionStatus(); 333 if (networkStatus == null 334 || networkStatus.getNetworkSelectionStatus() == NETWORK_SELECTION_ENABLED) { 335 return false; 336 } 337 return networkStatus.getNetworkSelectionDisableReason() 338 == WifiConfiguration.NetworkSelectionStatus.DISABLED_BY_WRONG_PASSWORD; 339 } 340 isHexString(String password)341 private static boolean isHexString(String password) { 342 return HEX_PATTERN.matcher(password).matches(); 343 } 344 345 /** 346 * Gets the security value from a ScanResult. 347 * 348 * @return related security value based on {@link WifiEntry} 349 */ getWifiEntrySecurity(ScanResult result)350 public static int getWifiEntrySecurity(ScanResult result) { 351 if (result.capabilities.contains("WEP")) { 352 return WifiEntry.SECURITY_WEP; 353 } else if (result.capabilities.contains("SAE")) { 354 return WifiEntry.SECURITY_SAE; 355 } else if (result.capabilities.contains("PSK")) { 356 return WifiEntry.SECURITY_PSK; 357 } else if (result.capabilities.contains("EAP_SUITE_B_192")) { 358 return WifiEntry.SECURITY_EAP_SUITE_B; 359 } else if (result.capabilities.contains("EAP")) { 360 return WifiEntry.SECURITY_EAP; 361 } else if (result.capabilities.contains("OWE")) { 362 return WifiEntry.SECURITY_OWE; 363 } 364 return WifiEntry.SECURITY_NONE; 365 } 366 367 /** 368 * Creates an instance of WifiPickerTracker using the default MAX_SCAN_AGE and 369 * SCAN_INTERVAL values. 370 */ createWifiPickerTracker( Lifecycle lifecycle, Context context, Handler mainHandler, Handler workerHandler, WifiPickerTracker.WifiPickerTrackerCallback listener)371 public static WifiPickerTracker createWifiPickerTracker( 372 Lifecycle lifecycle, Context context, 373 Handler mainHandler, Handler workerHandler, 374 WifiPickerTracker.WifiPickerTrackerCallback listener) { 375 return createWifiPickerTracker(lifecycle, context, mainHandler, workerHandler, 376 DEFAULT_MAX_SCAN_AGE_MILLIS, DEFAULT_SCAN_INTERVAL_MILLIS, listener); 377 } 378 379 /** 380 * Creates an instance of WifiPickerTracker. 381 */ createWifiPickerTracker( Lifecycle lifecycle, Context context, Handler mainHandler, Handler workerHandler, long maxScanAgeMillis, long scanIntervalMillis, WifiPickerTracker.WifiPickerTrackerCallback listener)382 public static WifiPickerTracker createWifiPickerTracker( 383 Lifecycle lifecycle, Context context, 384 Handler mainHandler, Handler workerHandler, 385 long maxScanAgeMillis, long scanIntervalMillis, 386 WifiPickerTracker.WifiPickerTrackerCallback listener) { 387 return new WifiPickerTracker( 388 lifecycle, context, 389 context.getSystemService(WifiManager.class), 390 context.getSystemService(ConnectivityManager.class), 391 mainHandler, workerHandler, ELAPSED_REALTIME_CLOCK, 392 maxScanAgeMillis, scanIntervalMillis, 393 listener); 394 } 395 396 /** 397 * Creates an instance of NetworkDetailsTracker using the default MAX_SCAN_AGE and 398 * SCAN_INTERVAL values. 399 */ createNetworkDetailsTracker( Lifecycle lifecycle, Context context, Handler mainHandler, Handler workerHandler, String key)400 public static NetworkDetailsTracker createNetworkDetailsTracker( 401 Lifecycle lifecycle, Context context, 402 Handler mainHandler, Handler workerHandler, 403 String key) { 404 return createNetworkDetailsTracker(lifecycle, context, mainHandler, workerHandler, 405 DEFAULT_MAX_SCAN_AGE_MILLIS, DEFAULT_SCAN_INTERVAL_MILLIS, key); 406 } 407 408 /** 409 * Creates an instance of NetworkDetailsTracker. 410 */ createNetworkDetailsTracker( Lifecycle lifecycle, Context context, Handler mainHandler, Handler workerHandler, long maxScanAgeMillis, long scanIntervalMillis, String key)411 public static NetworkDetailsTracker createNetworkDetailsTracker( 412 Lifecycle lifecycle, Context context, 413 Handler mainHandler, Handler workerHandler, 414 long maxScanAgeMillis, long scanIntervalMillis, 415 String key) { 416 return NetworkDetailsTracker.createNetworkDetailsTracker( 417 lifecycle, context, 418 context.getSystemService(WifiManager.class), 419 context.getSystemService(ConnectivityManager.class), 420 mainHandler, workerHandler, ELAPSED_REALTIME_CLOCK, 421 maxScanAgeMillis, scanIntervalMillis, 422 key); 423 } 424 425 /** 426 * Shows {@code ActionDisabledByAdminDialog} when the action is disallowed by 427 * a device owner or a profile owner. Otherwise, a {@code Toast} will be shown to inform the 428 * user that the action is disabled. 429 */ 430 // TODO(b/186905050): add unit tests for this class and {@code PreferenceController} that uses 431 // this method. runClickableWhileDisabled(Context context, String restriction, FragmentController fragmentController)432 public static void runClickableWhileDisabled(Context context, String restriction, 433 FragmentController fragmentController) { 434 if (hasUserRestrictionByDpm(context, restriction)) { 435 showActionDisabledByAdminDialog(context, restriction, fragmentController); 436 } else { 437 Toast.makeText(context, context.getString(R.string.action_unavailable), 438 Toast.LENGTH_LONG).show(); 439 } 440 } 441 442 /** 443 * Shows {@code ActionDisabledByAdminDialog} when the action is disallowed with 444 * {@code DISALLOW_CONFIG_WIFI} restriction by DevicePolicyManager. Otherwise, a {@code Toast} 445 * will be shown to inform the user that the action is disabled. 446 */ runClickableWhileDisabled(Context context, FragmentController fragmentController)447 public static void runClickableWhileDisabled(Context context, 448 FragmentController fragmentController) { 449 runClickableWhileDisabled(context, DISALLOW_CONFIG_WIFI, fragmentController); 450 } 451 452 /** 453 * Shows ActionDisabledByAdminDialog when there is user restriction set by device policy 454 * manager. 455 */ 456 // TODO(b/186905050): add unit tests for this class and {@code PreferenceController} that uses 457 // this method. showActionDisabledByAdminDialog(Context context, String restriction, FragmentController fragmentController)458 public static void showActionDisabledByAdminDialog(Context context, String restriction, 459 FragmentController fragmentController) { 460 fragmentController.showDialog( 461 EnterpriseUtils.getActionDisabledByAdminDialog(context, restriction), 462 DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG); 463 } 464 } 465