1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
5  *
6  *      http://www.apache.org/licenses/LICENSE-2.0
7  *
8  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
9  */
10 
11 package com.android.settingslib.wifi;
12 
13 import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
14 import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
15 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
16 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
17 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
18 
19 import android.annotation.SuppressLint;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.net.ConnectivityManager;
23 import android.net.ConnectivityManager.NetworkCallback;
24 import android.net.Network;
25 import android.net.NetworkCapabilities;
26 import android.net.NetworkInfo;
27 import android.net.NetworkKey;
28 import android.net.NetworkRequest;
29 import android.net.NetworkScoreManager;
30 import android.net.ScoredNetwork;
31 import android.net.TransportInfo;
32 import android.net.vcn.VcnTransportInfo;
33 import android.net.wifi.WifiInfo;
34 import android.net.wifi.WifiManager;
35 import android.net.wifi.WifiNetworkScoreCache;
36 import android.os.Handler;
37 import android.os.HandlerThread;
38 import android.os.Looper;
39 import android.provider.Settings;
40 
41 import androidx.annotation.Nullable;
42 
43 import com.android.settingslib.R;
44 
45 import java.io.PrintWriter;
46 import java.text.SimpleDateFormat;
47 import java.util.HashSet;
48 import java.util.List;
49 import java.util.Set;
50 
51 /**
52  * Track status of Wi-Fi for the Sys UI.
53  */
54 @SuppressLint("MissingPermission")
55 public class WifiStatusTracker {
56     private static final int HISTORY_SIZE = 32;
57     private static final SimpleDateFormat SSDF = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
58     private final Context mContext;
59     private final WifiNetworkScoreCache mWifiNetworkScoreCache;
60     private final WifiManager mWifiManager;
61     private final NetworkScoreManager mNetworkScoreManager;
62     private final ConnectivityManager mConnectivityManager;
63     private final Handler mHandler;
64     private final Handler mMainThreadHandler;
65     private final Set<Integer> mNetworks = new HashSet<>();
66     private int mPrimaryNetworkId;
67     // Save the previous HISTORY_SIZE states for logging.
68     private final String[] mHistory = new String[HISTORY_SIZE];
69     // Where to copy the next state into.
70     private int mHistoryIndex;
71     private final WifiNetworkScoreCache.CacheListener mCacheListener;
72     private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder()
73             .clearCapabilities()
74             .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
75             .addTransportType(TRANSPORT_WIFI)
76             .addTransportType(TRANSPORT_CELLULAR)
77             .build();
78     private final NetworkCallback mNetworkCallback =
79             new NetworkCallback(NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) {
80         // Note: onCapabilitiesChanged is guaranteed to be called "immediately" after onAvailable
81         // and onLinkPropertiesChanged.
82         @Override
83         public void onCapabilitiesChanged(
84                 Network network, NetworkCapabilities networkCapabilities) {
85             WifiInfo wifiInfo = getMainOrUnderlyingWifiInfo(networkCapabilities);
86             boolean isWifi = connectionIsWifi(networkCapabilities, wifiInfo);
87             // As long as it is a WiFi network, we will log it in the dumpsys for debugging.
88             if (isWifi) {
89                 String log = new StringBuilder()
90                         .append(SSDF.format(System.currentTimeMillis())).append(",")
91                         .append("onCapabilitiesChanged: ")
92                         .append("network=").append(network).append(",")
93                         .append("networkCapabilities=").append(networkCapabilities)
94                         .toString();
95                 recordLastWifiNetwork(log);
96             }
97             // Ignore the WiFi network if it doesn't contain any valid WifiInfo, or it is not the
98             // primary WiFi.
99             if (wifiInfo == null || !wifiInfo.isPrimary()) {
100                 // Remove the network from the tracking list once it becomes non-primary.
101                 if (mNetworks.contains(network.getNetId())) {
102                     mNetworks.remove(network.getNetId());
103                 }
104                 return;
105             }
106             if (!mNetworks.contains(network.getNetId())) {
107                 mNetworks.add(network.getNetId());
108             }
109             mPrimaryNetworkId = network.getNetId();
110             updateWifiInfo(wifiInfo);
111             updateStatusLabel();
112             mMainThreadHandler.post(() -> postResults());
113         }
114 
115         @Override
116         public void onLost(Network network) {
117             String log = new StringBuilder()
118                     .append(SSDF.format(System.currentTimeMillis())).append(",")
119                     .append("onLost: ")
120                     .append("network=").append(network)
121                     .toString();
122             recordLastWifiNetwork(log);
123             if (mNetworks.contains(network.getNetId())) {
124                 mNetworks.remove(network.getNetId());
125             }
126             if (network.getNetId() != mPrimaryNetworkId) {
127                 return;
128             }
129             updateWifiInfo(null);
130             updateStatusLabel();
131             mMainThreadHandler.post(() -> postResults());
132         }
133     };
134     private final NetworkCallback mDefaultNetworkCallback =
135             new NetworkCallback(NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) {
136         @Override
137         public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
138             // network is now the default network, and its capabilities are nc.
139             // This method will always be called immediately after the network becomes the
140             // default, in addition to any time the capabilities change while the network is
141             // the default.
142             mDefaultNetwork = network;
143             mDefaultNetworkCapabilities = nc;
144             updateStatusLabel();
145             mMainThreadHandler.post(() -> postResults());
146         }
147         @Override
148         public void onLost(Network network) {
149             // The system no longer has a default network.
150             mDefaultNetwork = null;
151             mDefaultNetworkCapabilities = null;
152             updateStatusLabel();
153             mMainThreadHandler.post(() -> postResults());
154         }
155     };
156     private Network mDefaultNetwork = null;
157     private NetworkCapabilities mDefaultNetworkCapabilities = null;
158     private final Runnable mCallback;
159 
160     private WifiInfo mWifiInfo;
161     public boolean enabled;
162     public boolean isCaptivePortal;
163     public boolean isDefaultNetwork;
164     public boolean isCarrierMerged;
165     public int subId;
166     public int state;
167     public boolean connected;
168     public String ssid;
169     public int rssi;
170     public int level;
171     public String statusLabel;
172 
WifiStatusTracker(Context context, WifiManager wifiManager, NetworkScoreManager networkScoreManager, ConnectivityManager connectivityManager, Runnable callback)173     public WifiStatusTracker(Context context, WifiManager wifiManager,
174             NetworkScoreManager networkScoreManager, ConnectivityManager connectivityManager,
175             Runnable callback) {
176         this(context, wifiManager, networkScoreManager, connectivityManager, callback, null, null);
177     }
178 
WifiStatusTracker(Context context, WifiManager wifiManager, NetworkScoreManager networkScoreManager, ConnectivityManager connectivityManager, Runnable callback, Handler foregroundHandler, Handler backgroundHandler)179     public WifiStatusTracker(Context context, WifiManager wifiManager,
180             NetworkScoreManager networkScoreManager, ConnectivityManager connectivityManager,
181             Runnable callback, Handler foregroundHandler, Handler backgroundHandler) {
182         mContext = context;
183         mWifiManager = wifiManager;
184         mWifiNetworkScoreCache = new WifiNetworkScoreCache(context);
185         mNetworkScoreManager = networkScoreManager;
186         mConnectivityManager = connectivityManager;
187         mCallback = callback;
188         if (backgroundHandler == null) {
189             HandlerThread handlerThread = new HandlerThread("WifiStatusTrackerHandler");
190             handlerThread.start();
191             mHandler = new Handler(handlerThread.getLooper());
192         } else {
193             mHandler = backgroundHandler;
194         }
195         mMainThreadHandler = foregroundHandler == null
196                 ? new Handler(Looper.getMainLooper()) : foregroundHandler;
197         mCacheListener =
198                 new WifiNetworkScoreCache.CacheListener(mHandler) {
199                     @Override
200                     public void networkCacheUpdated(List<ScoredNetwork> updatedNetworks) {
201                         updateStatusLabel();
202                         mMainThreadHandler.post(() -> postResults());
203                     }
204                 };
205     }
206 
setListening(boolean listening)207     public void setListening(boolean listening) {
208         if (listening) {
209             mNetworkScoreManager.registerNetworkScoreCache(NetworkKey.TYPE_WIFI,
210                     mWifiNetworkScoreCache, NetworkScoreManager.SCORE_FILTER_CURRENT_NETWORK);
211             mWifiNetworkScoreCache.registerListener(mCacheListener);
212             mConnectivityManager.registerNetworkCallback(
213                     mNetworkRequest, mNetworkCallback, mHandler);
214             mConnectivityManager.registerDefaultNetworkCallback(mDefaultNetworkCallback, mHandler);
215         } else {
216             mNetworkScoreManager.unregisterNetworkScoreCache(NetworkKey.TYPE_WIFI,
217                     mWifiNetworkScoreCache);
218             mWifiNetworkScoreCache.unregisterListener();
219             mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
220             mConnectivityManager.unregisterNetworkCallback(mDefaultNetworkCallback);
221         }
222     }
223 
224     /**
225      * Fetches initial state as if a WifiManager.NETWORK_STATE_CHANGED_ACTION have been received.
226      * This replaces the dependency on the initial sticky broadcast.
227      */
fetchInitialState()228     public void fetchInitialState() {
229         if (mWifiManager == null) {
230             return;
231         }
232         updateWifiState();
233         final NetworkInfo networkInfo =
234                 mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
235         connected = networkInfo != null && networkInfo.isConnected();
236         mWifiInfo = null;
237         ssid = null;
238         if (connected) {
239             mWifiInfo = mWifiManager.getConnectionInfo();
240             if (mWifiInfo != null) {
241                 if (mWifiInfo.isPasspointAp() || mWifiInfo.isOsuAp()) {
242                     ssid = mWifiInfo.getPasspointProviderFriendlyName();
243                 } else {
244                     ssid = getValidSsid(mWifiInfo);
245                 }
246                 isCarrierMerged = mWifiInfo.isCarrierMerged();
247                 subId = mWifiInfo.getSubscriptionId();
248                 updateRssi(mWifiInfo.getRssi());
249                 maybeRequestNetworkScore();
250             }
251         }
252         updateStatusLabel();
253     }
254 
handleBroadcast(Intent intent)255     public void handleBroadcast(Intent intent) {
256         if (mWifiManager == null) {
257             return;
258         }
259         String action = intent.getAction();
260         if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) {
261             updateWifiState();
262         }
263     }
264 
updateWifiInfo(WifiInfo wifiInfo)265     private void updateWifiInfo(WifiInfo wifiInfo) {
266         updateWifiState();
267         connected = wifiInfo != null;
268         mWifiInfo = wifiInfo;
269         ssid = null;
270         if (mWifiInfo != null) {
271             if (mWifiInfo.isPasspointAp() || mWifiInfo.isOsuAp()) {
272                 ssid = mWifiInfo.getPasspointProviderFriendlyName();
273             } else {
274                 ssid = getValidSsid(mWifiInfo);
275             }
276             isCarrierMerged = mWifiInfo.isCarrierMerged();
277             subId = mWifiInfo.getSubscriptionId();
278             updateRssi(mWifiInfo.getRssi());
279             maybeRequestNetworkScore();
280         }
281     }
282 
updateWifiState()283     private void updateWifiState() {
284         state = mWifiManager.getWifiState();
285         enabled = state == WifiManager.WIFI_STATE_ENABLED;
286     }
287 
updateRssi(int newRssi)288     private void updateRssi(int newRssi) {
289         rssi = newRssi;
290         level = mWifiManager.calculateSignalLevel(rssi);
291     }
292 
maybeRequestNetworkScore()293     private void maybeRequestNetworkScore() {
294         NetworkKey networkKey = NetworkKey.createFromWifiInfo(mWifiInfo);
295         if (mWifiNetworkScoreCache.getScoredNetwork(networkKey) == null) {
296             mNetworkScoreManager.requestScores(new NetworkKey[]{ networkKey });
297         }
298     }
299 
updateStatusLabel()300     private void updateStatusLabel() {
301         if (mWifiManager == null) {
302             return;
303         }
304         NetworkCapabilities networkCapabilities;
305         isDefaultNetwork = mDefaultNetworkCapabilities != null
306                 && connectionIsWifi(mDefaultNetworkCapabilities);
307         if (isDefaultNetwork) {
308             // Wifi is connected and the default network.
309             networkCapabilities = mDefaultNetworkCapabilities;
310         } else {
311             networkCapabilities = mConnectivityManager.getNetworkCapabilities(
312                     mWifiManager.getCurrentNetwork());
313         }
314         isCaptivePortal = false;
315         if (networkCapabilities != null) {
316             if (networkCapabilities.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) {
317                 statusLabel = mContext.getString(R.string.wifi_status_sign_in_required);
318                 isCaptivePortal = true;
319                 return;
320             } else if (networkCapabilities.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY)) {
321                 statusLabel = mContext.getString(R.string.wifi_limited_connection);
322                 return;
323             } else if (!networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)) {
324                 final String mode = Settings.Global.getString(mContext.getContentResolver(),
325                         Settings.Global.PRIVATE_DNS_MODE);
326                 if (networkCapabilities.isPrivateDnsBroken()) {
327                     statusLabel = mContext.getString(R.string.private_dns_broken);
328                 } else {
329                     statusLabel = mContext.getString(R.string.wifi_status_no_internet);
330                 }
331                 return;
332             } else if (!isDefaultNetwork && mDefaultNetworkCapabilities != null
333                     && mDefaultNetworkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
334                 statusLabel = mContext.getString(R.string.wifi_connected_low_quality);
335                 return;
336             }
337         }
338 
339         ScoredNetwork scoredNetwork =
340                 mWifiNetworkScoreCache.getScoredNetwork(NetworkKey.createFromWifiInfo(mWifiInfo));
341         statusLabel = scoredNetwork == null
342                 ? null : AccessPoint.getSpeedLabel(mContext, scoredNetwork, rssi);
343     }
344 
345     @Nullable
getMainOrUnderlyingWifiInfo( @ullable NetworkCapabilities networkCapabilities)346     private WifiInfo getMainOrUnderlyingWifiInfo(
347             @Nullable NetworkCapabilities networkCapabilities) {
348         if (networkCapabilities == null) {
349             return null;
350         }
351 
352         WifiInfo mainWifiInfo = getMainWifiInfo(networkCapabilities);
353         if (mainWifiInfo != null) {
354             return mainWifiInfo;
355         }
356 
357         // Only CELLULAR networks may have underlying wifi information that's relevant to SysUI,
358         // so skip the underlying network check if it's not CELLULAR.
359         if (!networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
360             return mainWifiInfo;
361         }
362 
363         List<Network> underlyingNetworks = networkCapabilities.getUnderlyingNetworks();
364         if (underlyingNetworks == null) {
365             return null;
366         }
367 
368         // Some connections, like VPN connections, may have underlying networks that are
369         // eventually traced to a wifi or carrier merged connection. So, check those underlying
370         // networks for possible wifi information as well. See b/225902574.
371         for (Network underlyingNetwork : underlyingNetworks) {
372             NetworkCapabilities underlyingNetworkCapabilities =
373                     mConnectivityManager.getNetworkCapabilities(underlyingNetwork);
374             WifiInfo underlyingWifiInfo = getMainWifiInfo(underlyingNetworkCapabilities);
375             if (underlyingWifiInfo != null) {
376                 return underlyingWifiInfo;
377             }
378         }
379 
380         return null;
381     }
382 
383     @Nullable
getMainWifiInfo(@ullable NetworkCapabilities networkCapabilities)384     private WifiInfo getMainWifiInfo(@Nullable NetworkCapabilities networkCapabilities) {
385         if (networkCapabilities == null) {
386             return null;
387         }
388         boolean canHaveWifiInfo = networkCapabilities.hasTransport(TRANSPORT_WIFI)
389                 || networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
390         if (!canHaveWifiInfo) {
391             return null;
392         }
393 
394         TransportInfo transportInfo = networkCapabilities.getTransportInfo();
395         if (transportInfo instanceof VcnTransportInfo) {
396             // This VcnTransportInfo logic is copied from
397             // [com.android.settingslib.Utils.tryGetWifiInfoForVcn]. It's copied instead of
398             // re-used because it makes the logic here clearer.
399             return ((VcnTransportInfo) transportInfo).getWifiInfo();
400         } else if (transportInfo instanceof WifiInfo) {
401             return (WifiInfo) transportInfo;
402         } else {
403             return null;
404         }
405     }
406 
connectionIsWifi(NetworkCapabilities networkCapabilities)407     private boolean connectionIsWifi(NetworkCapabilities networkCapabilities) {
408         return connectionIsWifi(
409                 networkCapabilities,
410                 getMainOrUnderlyingWifiInfo(networkCapabilities));
411     }
412 
connectionIsWifi( @ullable NetworkCapabilities networkCapabilities, WifiInfo wifiInfo)413     private boolean connectionIsWifi(
414             @Nullable NetworkCapabilities networkCapabilities, WifiInfo wifiInfo) {
415         if (networkCapabilities == null) {
416             return false;
417         }
418         return wifiInfo != null || networkCapabilities.hasTransport(TRANSPORT_WIFI);
419     }
420 
421     /** Refresh the status label on Locale changed. */
refreshLocale()422     public void refreshLocale() {
423         updateStatusLabel();
424         mMainThreadHandler.post(() -> postResults());
425     }
426 
getValidSsid(WifiInfo info)427     private String getValidSsid(WifiInfo info) {
428         String ssid = info.getSSID();
429         if (ssid != null && !WifiManager.UNKNOWN_SSID.equals(ssid)) {
430             return ssid;
431         }
432         return null;
433     }
434 
recordLastWifiNetwork(String log)435     private void recordLastWifiNetwork(String log) {
436         mHistory[mHistoryIndex] = log;
437         mHistoryIndex = (mHistoryIndex + 1) % HISTORY_SIZE;
438     }
439 
postResults()440     private void postResults() {
441         mCallback.run();
442     }
443 
444     /** Dump function. */
dump(PrintWriter pw)445     public void dump(PrintWriter pw) {
446         pw.println("  - WiFi Network History ------");
447         int size = 0;
448         for (int i = 0; i < HISTORY_SIZE; i++) {
449             if (mHistory[i] != null) size++;
450         }
451         // Print out the previous states in ordered number.
452         for (int i = mHistoryIndex + HISTORY_SIZE - 1;
453                 i >= mHistoryIndex + HISTORY_SIZE - size; i--) {
454             pw.println("  Previous WiFiNetwork("
455                     + (mHistoryIndex + HISTORY_SIZE - i) + "): "
456                     + mHistory[i & (HISTORY_SIZE - 1)]);
457         }
458     }
459 }
460