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