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(
335                         com.android.wifitrackerlib.R.string.wifi_connected_low_quality);
336                 return;
337             }
338         }
339 
340         ScoredNetwork scoredNetwork =
341                 mWifiNetworkScoreCache.getScoredNetwork(NetworkKey.createFromWifiInfo(mWifiInfo));
342         statusLabel = scoredNetwork == null
343                 ? null : AccessPoint.getSpeedLabel(mContext, scoredNetwork, rssi);
344     }
345 
346     @Nullable
getMainOrUnderlyingWifiInfo( @ullable NetworkCapabilities networkCapabilities)347     private WifiInfo getMainOrUnderlyingWifiInfo(
348             @Nullable NetworkCapabilities networkCapabilities) {
349         if (networkCapabilities == null) {
350             return null;
351         }
352 
353         WifiInfo mainWifiInfo = getMainWifiInfo(networkCapabilities);
354         if (mainWifiInfo != null) {
355             return mainWifiInfo;
356         }
357 
358         // Only CELLULAR networks may have underlying wifi information that's relevant to SysUI,
359         // so skip the underlying network check if it's not CELLULAR.
360         if (!networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
361             return mainWifiInfo;
362         }
363 
364         List<Network> underlyingNetworks = networkCapabilities.getUnderlyingNetworks();
365         if (underlyingNetworks == null) {
366             return null;
367         }
368 
369         // Some connections, like VPN connections, may have underlying networks that are
370         // eventually traced to a wifi or carrier merged connection. So, check those underlying
371         // networks for possible wifi information as well. See b/225902574.
372         for (Network underlyingNetwork : underlyingNetworks) {
373             NetworkCapabilities underlyingNetworkCapabilities =
374                     mConnectivityManager.getNetworkCapabilities(underlyingNetwork);
375             WifiInfo underlyingWifiInfo = getMainWifiInfo(underlyingNetworkCapabilities);
376             if (underlyingWifiInfo != null) {
377                 return underlyingWifiInfo;
378             }
379         }
380 
381         return null;
382     }
383 
384     @Nullable
getMainWifiInfo(@ullable NetworkCapabilities networkCapabilities)385     private WifiInfo getMainWifiInfo(@Nullable NetworkCapabilities networkCapabilities) {
386         if (networkCapabilities == null) {
387             return null;
388         }
389         boolean canHaveWifiInfo = networkCapabilities.hasTransport(TRANSPORT_WIFI)
390                 || networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
391         if (!canHaveWifiInfo) {
392             return null;
393         }
394 
395         TransportInfo transportInfo = networkCapabilities.getTransportInfo();
396         if (transportInfo instanceof VcnTransportInfo) {
397             // This VcnTransportInfo logic is copied from
398             // [com.android.settingslib.Utils.tryGetWifiInfoForVcn]. It's copied instead of
399             // re-used because it makes the logic here clearer.
400             return ((VcnTransportInfo) transportInfo).getWifiInfo();
401         } else if (transportInfo instanceof WifiInfo) {
402             return (WifiInfo) transportInfo;
403         } else {
404             return null;
405         }
406     }
407 
connectionIsWifi(NetworkCapabilities networkCapabilities)408     private boolean connectionIsWifi(NetworkCapabilities networkCapabilities) {
409         return connectionIsWifi(
410                 networkCapabilities,
411                 getMainOrUnderlyingWifiInfo(networkCapabilities));
412     }
413 
connectionIsWifi( @ullable NetworkCapabilities networkCapabilities, WifiInfo wifiInfo)414     private boolean connectionIsWifi(
415             @Nullable NetworkCapabilities networkCapabilities, WifiInfo wifiInfo) {
416         if (networkCapabilities == null) {
417             return false;
418         }
419         return wifiInfo != null || networkCapabilities.hasTransport(TRANSPORT_WIFI);
420     }
421 
422     /** Refresh the status label on Locale changed. */
refreshLocale()423     public void refreshLocale() {
424         updateStatusLabel();
425         mMainThreadHandler.post(() -> postResults());
426     }
427 
getValidSsid(WifiInfo info)428     private String getValidSsid(WifiInfo info) {
429         String ssid = info.getSSID();
430         if (ssid != null && !WifiManager.UNKNOWN_SSID.equals(ssid)) {
431             return ssid;
432         }
433         return null;
434     }
435 
recordLastWifiNetwork(String log)436     private void recordLastWifiNetwork(String log) {
437         mHistory[mHistoryIndex] = log;
438         mHistoryIndex = (mHistoryIndex + 1) % HISTORY_SIZE;
439     }
440 
postResults()441     private void postResults() {
442         mCallback.run();
443     }
444 
445     /** Dump function. */
dump(PrintWriter pw)446     public void dump(PrintWriter pw) {
447         pw.println("  - WiFi Network History ------");
448         int size = 0;
449         for (int i = 0; i < HISTORY_SIZE; i++) {
450             if (mHistory[i] != null) size++;
451         }
452         // Print out the previous states in ordered number.
453         for (int i = mHistoryIndex + HISTORY_SIZE - 1;
454                 i >= mHistoryIndex + HISTORY_SIZE - size; i--) {
455             pw.println("  Previous WiFiNetwork("
456                     + (mHistoryIndex + HISTORY_SIZE - i) + "): "
457                     + mHistory[i & (HISTORY_SIZE - 1)]);
458         }
459     }
460 }
461