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 
17 package com.android.settingslib.wifi;
18 
19 import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLED;
20 import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.getMaxNetworkSelectionDisableReason;
21 
22 import android.content.Context;
23 import android.content.Intent;
24 import android.graphics.drawable.Drawable;
25 import android.icu.text.MessageFormat;
26 import android.net.wifi.ScanResult;
27 import android.net.wifi.WifiConfiguration;
28 import android.net.wifi.WifiConfiguration.NetworkSelectionStatus;
29 import android.net.wifi.WifiInfo;
30 import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo;
31 import android.os.Bundle;
32 import android.os.SystemClock;
33 import android.util.Log;
34 
35 import androidx.annotation.VisibleForTesting;
36 
37 import com.android.settingslib.R;
38 
39 import java.util.HashMap;
40 import java.util.Locale;
41 import java.util.Map;
42 
43 public class WifiUtils {
44 
45     private static final String TAG = "WifiUtils";
46 
47     private static final int INVALID_RSSI = -127;
48 
49     /**
50      * The intent action shows Wi-Fi dialog to connect Wi-Fi network.
51      * <p>
52      * Input: The calling package should put the chosen
53      * com.android.wifitrackerlib.WifiEntry#getKey() to a string extra in the request bundle into
54      * the {@link #EXTRA_CHOSEN_WIFI_ENTRY_KEY}.
55      * <p>
56      * Output: Nothing.
57      */
58     @VisibleForTesting
59     static final String ACTION_WIFI_DIALOG = "com.android.settings.WIFI_DIALOG";
60 
61     /**
62      * Specify a key that indicates the WifiEntry to be configured.
63      */
64     @VisibleForTesting
65     static final String EXTRA_CHOSEN_WIFI_ENTRY_KEY = "key_chosen_wifientry_key";
66 
67     /**
68      * The lookup key for a boolean that indicates whether a chosen WifiEntry request to connect to.
69      * {@code true} means a chosen WifiEntry request to connect to.
70      */
71     @VisibleForTesting
72     static final String EXTRA_CONNECT_FOR_CALLER = "connect_for_caller";
73 
74     /**
75      * The intent action shows network details settings to allow configuration of Wi-Fi.
76      * <p>
77      * In some cases, a matching Activity may not exist, so ensure you
78      * safeguard against this.
79      * <p>
80      * Input: The calling package should put the chosen
81      * com.android.wifitrackerlib.WifiEntry#getKey() to a string extra in the request bundle into
82      * the {@link #KEY_CHOSEN_WIFIENTRY_KEY}.
83      * <p>
84      * Output: Nothing.
85      */
86     public static final String ACTION_WIFI_DETAILS_SETTINGS =
87             "android.settings.WIFI_DETAILS_SETTINGS";
88     public static final String KEY_CHOSEN_WIFIENTRY_KEY = "key_chosen_wifientry_key";
89     public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args";
90 
91     static final int[] WIFI_PIE = {
92             com.android.internal.R.drawable.ic_wifi_signal_0,
93             com.android.internal.R.drawable.ic_wifi_signal_1,
94             com.android.internal.R.drawable.ic_wifi_signal_2,
95             com.android.internal.R.drawable.ic_wifi_signal_3,
96             com.android.internal.R.drawable.ic_wifi_signal_4
97     };
98 
99     static final int[] NO_INTERNET_WIFI_PIE = {
100             R.drawable.ic_no_internet_wifi_signal_0,
101             R.drawable.ic_no_internet_wifi_signal_1,
102             R.drawable.ic_no_internet_wifi_signal_2,
103             R.drawable.ic_no_internet_wifi_signal_3,
104             R.drawable.ic_no_internet_wifi_signal_4
105     };
106 
buildLoggingSummary(AccessPoint accessPoint, WifiConfiguration config)107     public static String buildLoggingSummary(AccessPoint accessPoint, WifiConfiguration config) {
108         final StringBuilder summary = new StringBuilder();
109         final WifiInfo info = accessPoint.getInfo();
110         // Add RSSI/band information for this config, what was seen up to 6 seconds ago
111         // verbose WiFi Logging is only turned on thru developers settings
112         if (accessPoint.isActive() && info != null) {
113             summary.append(" f=" + Integer.toString(info.getFrequency()));
114         }
115         summary.append(" " + getVisibilityStatus(accessPoint));
116         if (config != null
117                 && (config.getNetworkSelectionStatus().getNetworkSelectionStatus()
118                         != NETWORK_SELECTION_ENABLED)) {
119             summary.append(" (" + config.getNetworkSelectionStatus().getNetworkStatusString());
120             if (config.getNetworkSelectionStatus().getDisableTime() > 0) {
121                 long now = System.currentTimeMillis();
122                 long diff = (now - config.getNetworkSelectionStatus().getDisableTime()) / 1000;
123                 long sec = diff % 60; //seconds
124                 long min = (diff / 60) % 60; //minutes
125                 long hour = (min / 60) % 60; //hours
126                 summary.append(", ");
127                 if (hour > 0) summary.append(Long.toString(hour) + "h ");
128                 summary.append(Long.toString(min) + "m ");
129                 summary.append(Long.toString(sec) + "s ");
130             }
131             summary.append(")");
132         }
133 
134         if (config != null) {
135             NetworkSelectionStatus networkStatus = config.getNetworkSelectionStatus();
136             for (int reason = 0; reason <= getMaxNetworkSelectionDisableReason(); reason++) {
137                 if (networkStatus.getDisableReasonCounter(reason) != 0) {
138                     summary.append(" ")
139                             .append(NetworkSelectionStatus
140                                     .getNetworkSelectionDisableReasonString(reason))
141                             .append("=")
142                             .append(networkStatus.getDisableReasonCounter(reason));
143                 }
144             }
145         }
146 
147         return summary.toString();
148     }
149 
150     /**
151      * Returns the visibility status of the WifiConfiguration.
152      *
153      * @return autojoin debugging information
154      * TODO: use a string formatter
155      * ["rssi 5Ghz", "num results on 5GHz" / "rssi 5Ghz", "num results on 5GHz"]
156      * For instance [-40,5/-30,2]
157      */
158     @VisibleForTesting
getVisibilityStatus(AccessPoint accessPoint)159     static String getVisibilityStatus(AccessPoint accessPoint) {
160         final WifiInfo info = accessPoint.getInfo();
161         StringBuilder visibility = new StringBuilder();
162         StringBuilder scans24GHz = new StringBuilder();
163         StringBuilder scans5GHz = new StringBuilder();
164         StringBuilder scans60GHz = new StringBuilder();
165         String bssid = null;
166 
167         if (accessPoint.isActive() && info != null) {
168             bssid = info.getBSSID();
169             if (bssid != null) {
170                 visibility.append(" ").append(bssid);
171             }
172             visibility.append(" standard = ").append(info.getWifiStandard());
173             visibility.append(" rssi=").append(info.getRssi());
174             visibility.append(" ");
175             visibility.append(" score=").append(info.getScore());
176             if (accessPoint.getSpeed() != AccessPoint.Speed.NONE) {
177                 visibility.append(" speed=").append(accessPoint.getSpeedLabel());
178             }
179             visibility.append(String.format(" tx=%.1f,", info.getSuccessfulTxPacketsPerSecond()));
180             visibility.append(String.format("%.1f,", info.getRetriedTxPacketsPerSecond()));
181             visibility.append(String.format("%.1f ", info.getLostTxPacketsPerSecond()));
182             visibility.append(String.format("rx=%.1f", info.getSuccessfulRxPacketsPerSecond()));
183         }
184 
185         int maxRssi5 = INVALID_RSSI;
186         int maxRssi24 = INVALID_RSSI;
187         int maxRssi60 = INVALID_RSSI;
188         final int maxDisplayedScans = 4;
189         int num5 = 0; // number of scanned BSSID on 5GHz band
190         int num24 = 0; // number of scanned BSSID on 2.4Ghz band
191         int num60 = 0; // number of scanned BSSID on 60Ghz band
192         int numBlockListed = 0;
193 
194         // TODO: sort list by RSSI or age
195         long nowMs = SystemClock.elapsedRealtime();
196         for (ScanResult result : accessPoint.getScanResults()) {
197             if (result == null) {
198                 continue;
199             }
200             if (result.frequency >= AccessPoint.LOWER_FREQ_5GHZ
201                     && result.frequency <= AccessPoint.HIGHER_FREQ_5GHZ) {
202                 // Strictly speaking: [4915, 5825]
203                 num5++;
204 
205                 if (result.level > maxRssi5) {
206                     maxRssi5 = result.level;
207                 }
208                 if (num5 <= maxDisplayedScans) {
209                     scans5GHz.append(
210                             verboseScanResultSummary(accessPoint, result, bssid,
211                                     nowMs));
212                 }
213             } else if (result.frequency >= AccessPoint.LOWER_FREQ_24GHZ
214                     && result.frequency <= AccessPoint.HIGHER_FREQ_24GHZ) {
215                 // Strictly speaking: [2412, 2482]
216                 num24++;
217 
218                 if (result.level > maxRssi24) {
219                     maxRssi24 = result.level;
220                 }
221                 if (num24 <= maxDisplayedScans) {
222                     scans24GHz.append(
223                             verboseScanResultSummary(accessPoint, result, bssid,
224                                     nowMs));
225                 }
226             } else if (result.frequency >= AccessPoint.LOWER_FREQ_60GHZ
227                     && result.frequency <= AccessPoint.HIGHER_FREQ_60GHZ) {
228                 // Strictly speaking: [60000, 61000]
229                 num60++;
230 
231                 if (result.level > maxRssi60) {
232                     maxRssi60 = result.level;
233                 }
234                 if (num60 <= maxDisplayedScans) {
235                     scans60GHz.append(
236                             verboseScanResultSummary(accessPoint, result, bssid,
237                                     nowMs));
238                 }
239             }
240         }
241         visibility.append(" [");
242         if (num24 > 0) {
243             visibility.append("(").append(num24).append(")");
244             if (num24 > maxDisplayedScans) {
245                 visibility.append("max=").append(maxRssi24).append(",");
246             }
247             visibility.append(scans24GHz.toString());
248         }
249         visibility.append(";");
250         if (num5 > 0) {
251             visibility.append("(").append(num5).append(")");
252             if (num5 > maxDisplayedScans) {
253                 visibility.append("max=").append(maxRssi5).append(",");
254             }
255             visibility.append(scans5GHz.toString());
256         }
257         visibility.append(";");
258         if (num60 > 0) {
259             visibility.append("(").append(num60).append(")");
260             if (num60 > maxDisplayedScans) {
261                 visibility.append("max=").append(maxRssi60).append(",");
262             }
263             visibility.append(scans60GHz.toString());
264         }
265         if (numBlockListed > 0) {
266             visibility.append("!").append(numBlockListed);
267         }
268         visibility.append("]");
269 
270         return visibility.toString();
271     }
272 
273     @VisibleForTesting
verboseScanResultSummary(AccessPoint accessPoint, ScanResult result, String bssid, long nowMs)274     /* package */ static String verboseScanResultSummary(AccessPoint accessPoint, ScanResult result,
275             String bssid, long nowMs) {
276         StringBuilder stringBuilder = new StringBuilder();
277         stringBuilder.append(" \n{").append(result.BSSID);
278         if (result.BSSID.equals(bssid)) {
279             stringBuilder.append("*");
280         }
281         stringBuilder.append("=").append(result.frequency);
282         stringBuilder.append(",").append(result.level);
283         int speed = getSpecificApSpeed(result, accessPoint.getScoredNetworkCache());
284         if (speed != AccessPoint.Speed.NONE) {
285             stringBuilder.append(",")
286                     .append(accessPoint.getSpeedLabel(speed));
287         }
288         int ageSeconds = (int) (nowMs - result.timestamp / 1000) / 1000;
289         stringBuilder.append(",").append(ageSeconds).append("s");
290         stringBuilder.append("}");
291         return stringBuilder.toString();
292     }
293 
294     @AccessPoint.Speed
getSpecificApSpeed(ScanResult result, Map<String, TimestampedScoredNetwork> scoredNetworkCache)295     private static int getSpecificApSpeed(ScanResult result,
296             Map<String, TimestampedScoredNetwork> scoredNetworkCache) {
297         TimestampedScoredNetwork timedScore = scoredNetworkCache.get(result.BSSID);
298         if (timedScore == null) {
299             return AccessPoint.Speed.NONE;
300         }
301         // For debugging purposes we may want to use mRssi rather than result.level as the average
302         // speed wil be determined by mRssi
303         return timedScore.getScore().calculateBadge(result.level);
304     }
305 
getMeteredLabel(Context context, WifiConfiguration config)306     public static String getMeteredLabel(Context context, WifiConfiguration config) {
307         // meteredOverride is whether the user manually set the metered setting or not.
308         // meteredHint is whether the network itself is telling us that it is metered
309         if (config.meteredOverride == WifiConfiguration.METERED_OVERRIDE_METERED
310                 || (config.meteredHint && !isMeteredOverridden(config))) {
311             return context.getString(R.string.wifi_metered_label);
312         }
313         return context.getString(R.string.wifi_unmetered_label);
314     }
315 
316     /**
317      * Returns the Internet icon resource for a given RSSI level.
318      *
319      * @param level The number of bars to show (0-4)
320      * @param noInternet True if a connected Wi-Fi network cannot access the Internet
321      */
getInternetIconResource(int level, boolean noInternet)322     public static int getInternetIconResource(int level, boolean noInternet) {
323         int wifiLevel = level;
324         if (wifiLevel < 0) {
325             Log.e(TAG, "Wi-Fi level is out of range! level:" + level);
326             wifiLevel = 0;
327         } else if (level >= WIFI_PIE.length) {
328             Log.e(TAG, "Wi-Fi level is out of range! level:" + level);
329             wifiLevel = WIFI_PIE.length - 1;
330         }
331         return noInternet ? NO_INTERNET_WIFI_PIE[wifiLevel] : WIFI_PIE[wifiLevel];
332     }
333 
334     /**
335      * Returns the Hotspot network icon resource.
336      *
337      * @param deviceType The device type of Hotspot network
338      */
getHotspotIconResource(int deviceType)339     public static int getHotspotIconResource(int deviceType) {
340         return switch (deviceType) {
341             case NetworkProviderInfo.DEVICE_TYPE_PHONE -> R.drawable.ic_hotspot_phone;
342             case NetworkProviderInfo.DEVICE_TYPE_TABLET -> R.drawable.ic_hotspot_tablet;
343             case NetworkProviderInfo.DEVICE_TYPE_LAPTOP -> R.drawable.ic_hotspot_laptop;
344             case NetworkProviderInfo.DEVICE_TYPE_WATCH -> R.drawable.ic_hotspot_watch;
345             case NetworkProviderInfo.DEVICE_TYPE_AUTO -> R.drawable.ic_hotspot_auto;
346             default -> R.drawable.ic_hotspot_phone;  // Return phone icon as default.
347         };
348     }
349 
350     /**
351      * Wrapper the {@link #getInternetIconResource} for testing compatibility.
352      */
353     public static class InternetIconInjector {
354 
355         protected final Context mContext;
356 
InternetIconInjector(Context context)357         public InternetIconInjector(Context context) {
358             mContext = context;
359         }
360 
361         /**
362          * Returns the Internet icon for a given RSSI level.
363          *
364          * @param noInternet True if a connected Wi-Fi network cannot access the Internet
365          * @param level The number of bars to show (0-4)
366          */
getIcon(boolean noInternet, int level)367         public Drawable getIcon(boolean noInternet, int level) {
368             return mContext.getDrawable(WifiUtils.getInternetIconResource(level, noInternet));
369         }
370     }
371 
isMeteredOverridden(WifiConfiguration config)372     public static boolean isMeteredOverridden(WifiConfiguration config) {
373         return config.meteredOverride != WifiConfiguration.METERED_OVERRIDE_NONE;
374     }
375 
376     /**
377      * Returns the Intent for Wi-Fi dialog.
378      *
379      * @param key              The Wi-Fi entry key
380      * @param connectForCaller True if a chosen WifiEntry request to connect to
381      */
getWifiDialogIntent(String key, boolean connectForCaller)382     public static Intent getWifiDialogIntent(String key, boolean connectForCaller) {
383         final Intent intent = new Intent(ACTION_WIFI_DIALOG);
384         intent.putExtra(EXTRA_CHOSEN_WIFI_ENTRY_KEY, key);
385         intent.putExtra(EXTRA_CONNECT_FOR_CALLER, connectForCaller);
386         return intent;
387     }
388 
389     /**
390      * Returns the Intent for Wi-Fi network details settings.
391      *
392      * @param key The Wi-Fi entry key
393      */
getWifiDetailsSettingsIntent(String key)394     public static Intent getWifiDetailsSettingsIntent(String key) {
395         final Intent intent = new Intent(ACTION_WIFI_DETAILS_SETTINGS);
396         final Bundle bundle = new Bundle();
397         bundle.putString(KEY_CHOSEN_WIFIENTRY_KEY, key);
398         intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
399         return intent;
400     }
401 
402     /**
403      * Returns the string of Wi-Fi tethering summary for connected devices.
404      *
405      * @param context          The application context
406      * @param connectedDevices The count of connected devices
407      */
getWifiTetherSummaryForConnectedDevices(Context context, int connectedDevices)408     public static String getWifiTetherSummaryForConnectedDevices(Context context,
409             int connectedDevices) {
410         MessageFormat msgFormat = new MessageFormat(
411                 context.getResources().getString(R.string.wifi_tether_connected_summary),
412                 Locale.getDefault());
413         Map<String, Object> arguments = new HashMap<>();
414         arguments.put("count", connectedDevices);
415         return msgFormat.format(arguments);
416     }
417 }
418