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