• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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