1 /*
2  * Copyright (C) 2019 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.settings.wifi.slice;
18 
19 import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
20 import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
21 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
22 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
23 
24 import static com.android.settings.wifi.slice.WifiSlice.DEFAULT_EXPANDED_ROW_COUNT;
25 
26 import android.content.Context;
27 import android.content.Intent;
28 import android.net.ConnectivityManager;
29 import android.net.ConnectivityManager.NetworkCallback;
30 import android.net.Network;
31 import android.net.NetworkCapabilities;
32 import android.net.NetworkRequest;
33 import android.net.Uri;
34 import android.net.wifi.WifiInfo;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.Looper;
38 import android.os.UserHandle;
39 import android.text.TextUtils;
40 import android.util.Log;
41 
42 import androidx.annotation.VisibleForTesting;
43 
44 import com.android.internal.util.Preconditions;
45 import com.android.settings.slices.SliceBackgroundWorker;
46 import com.android.settingslib.wifi.AccessPoint;
47 import com.android.settingslib.wifi.WifiTracker;
48 
49 import java.util.ArrayList;
50 import java.util.List;
51 
52 /**
53  * {@link SliceBackgroundWorker} for Wi-Fi, used by {@link WifiSlice}.
54  */
55 public class WifiScanWorker extends SliceBackgroundWorker<AccessPoint> implements
56         WifiTracker.WifiListener {
57 
58     private static final String TAG = "WifiScanWorker";
59 
60     @VisibleForTesting
61     WifiNetworkCallback mNetworkCallback;
62 
63     private final Context mContext;
64     private final ConnectivityManager mConnectivityManager;
65     private final WifiTracker mWifiTracker;
66 
67     private static String sClickedWifiSsid;
68 
WifiScanWorker(Context context, Uri uri)69     public WifiScanWorker(Context context, Uri uri) {
70         super(context, uri);
71         mContext = context;
72         mConnectivityManager = context.getSystemService(ConnectivityManager.class);
73         mWifiTracker = new WifiTracker(mContext, this /* wifiListener */,
74                 true /* includeSaved */, true /* includeScans */);
75     }
76 
77     @Override
onSlicePinned()78     protected void onSlicePinned() {
79         mWifiTracker.onStart();
80         onAccessPointsChanged();
81     }
82 
83     @Override
onSliceUnpinned()84     protected void onSliceUnpinned() {
85         mWifiTracker.onStop();
86         unregisterNetworkCallback();
87         clearClickedWifiOnSliceUnpinned();
88     }
89 
90     @Override
close()91     public void close() {
92         mWifiTracker.onDestroy();
93     }
94 
95     @Override
onWifiStateChanged(int state)96     public void onWifiStateChanged(int state) {
97         notifySliceChange();
98     }
99 
100     @Override
onConnectedChanged()101     public void onConnectedChanged() {
102     }
103 
104     @Override
onAccessPointsChanged()105     public void onAccessPointsChanged() {
106         // in case state has changed
107         if (!mWifiTracker.getManager().isWifiEnabled()) {
108             updateResults(null);
109             return;
110         }
111         // AccessPoints are sorted by the WifiTracker
112         final List<AccessPoint> accessPoints = mWifiTracker.getAccessPoints();
113         final List<AccessPoint> resultList = new ArrayList<>();
114         final int apRowCount = getApRowCount();
115         for (AccessPoint ap : accessPoints) {
116             if (ap.isReachable()) {
117                 resultList.add(clone(ap));
118                 if (resultList.size() >= apRowCount) {
119                     break;
120                 }
121             }
122         }
123         updateResults(resultList);
124     }
125 
getApRowCount()126     protected int getApRowCount() {
127         return DEFAULT_EXPANDED_ROW_COUNT;
128     }
129 
clone(AccessPoint accessPoint)130     private AccessPoint clone(AccessPoint accessPoint) {
131         final Bundle savedState = new Bundle();
132         accessPoint.saveWifiState(savedState);
133         return new AccessPoint(mContext, savedState);
134     }
135 
136     @Override
areListsTheSame(List<AccessPoint> a, List<AccessPoint> b)137     protected boolean areListsTheSame(List<AccessPoint> a, List<AccessPoint> b) {
138         if (!a.equals(b)) {
139             return false;
140         }
141 
142         // compare access point states one by one
143         final int listSize = a.size();
144         for (int i = 0; i < listSize; i++) {
145             if (a.get(i).getDetailedState() != b.get(i).getDetailedState()) {
146                 return false;
147             }
148         }
149         return true;
150     }
151 
saveClickedWifi(AccessPoint accessPoint)152     static void saveClickedWifi(AccessPoint accessPoint) {
153         sClickedWifiSsid = accessPoint.getSsidStr();
154     }
155 
clearClickedWifi()156     static void clearClickedWifi() {
157         sClickedWifiSsid = null;
158     }
159 
isWifiClicked(WifiInfo info)160     static boolean isWifiClicked(WifiInfo info) {
161         final String ssid = WifiInfo.sanitizeSsid(info.getSSID());
162         return !TextUtils.isEmpty(ssid) && TextUtils.equals(ssid, sClickedWifiSsid);
163     }
164 
clearClickedWifiOnSliceUnpinned()165     protected void clearClickedWifiOnSliceUnpinned() {
166         clearClickedWifi();
167     }
168 
isSessionValid()169     protected boolean isSessionValid() {
170         return true;
171     }
172 
registerNetworkCallback(Network wifiNetwork)173     public void registerNetworkCallback(Network wifiNetwork) {
174         if (wifiNetwork == null) {
175             return;
176         }
177 
178         if (mNetworkCallback != null && mNetworkCallback.isSameNetwork(wifiNetwork)) {
179             return;
180         }
181 
182         unregisterNetworkCallback();
183 
184         mNetworkCallback = new WifiNetworkCallback(wifiNetwork);
185         mConnectivityManager.registerNetworkCallback(
186                 new NetworkRequest.Builder()
187                         .clearCapabilities()
188                         .addTransportType(TRANSPORT_WIFI)
189                         .build(),
190                 mNetworkCallback,
191                 new Handler(Looper.getMainLooper()));
192     }
193 
unregisterNetworkCallback()194     public void unregisterNetworkCallback() {
195         if (mNetworkCallback != null) {
196             try {
197                 mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
198             } catch (RuntimeException e) {
199                 Log.e(TAG, "Unregistering CaptivePortalNetworkCallback failed.", e);
200             }
201             mNetworkCallback = null;
202         }
203     }
204 
205     class WifiNetworkCallback extends NetworkCallback {
206 
207         private final Network mNetwork;
208         private boolean mIsCaptivePortal;
209         private boolean mHasPartialConnectivity;
210         private boolean mIsValidated;
211 
WifiNetworkCallback(Network network)212         WifiNetworkCallback(Network network) {
213             mNetwork = Preconditions.checkNotNull(network);
214         }
215 
216         @Override
onCapabilitiesChanged(Network network, NetworkCapabilities nc)217         public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
218             if (!isSameNetwork(network)) {
219                 return;
220             }
221 
222             final boolean prevIsCaptivePortal = mIsCaptivePortal;
223             final boolean prevHasPartialConnectivity = mHasPartialConnectivity;
224             final boolean prevIsValidated = mIsValidated;
225 
226             mIsCaptivePortal = nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
227             mHasPartialConnectivity = nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY);
228             mIsValidated = nc.hasCapability(NET_CAPABILITY_VALIDATED);
229 
230             if (prevIsCaptivePortal == mIsCaptivePortal
231                     && prevHasPartialConnectivity == mHasPartialConnectivity
232                     && prevIsValidated == mIsValidated) {
233                 return;
234             }
235 
236             notifySliceChange();
237 
238             // Automatically start captive portal
239             if (!prevIsCaptivePortal && mIsCaptivePortal
240                     && isWifiClicked(mWifiTracker.getManager().getConnectionInfo())
241                     && isSessionValid()) {
242                 final Intent intent = new Intent(mContext, ConnectToWifiHandler.class)
243                         .putExtra(ConnectivityManager.EXTRA_NETWORK, network)
244                         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
245                 // Sending a broadcast in the system process needs to specify a user
246                 mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
247             }
248         }
249 
250         /**
251          * Returns true if the supplied network is not null and is the same as the originally
252          * supplied value.
253          */
isSameNetwork(Network network)254         public boolean isSameNetwork(Network network) {
255             return mNetwork.equals(network);
256         }
257     }
258 }
259