1 /*
2  * Copyright (C) 2020 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.wifitrackerlib;
18 
19 import static androidx.core.util.Preconditions.checkNotNull;
20 
21 import static com.android.wifitrackerlib.Utils.getBestScanResultByLevel;
22 import static com.android.wifitrackerlib.WifiEntry.ConnectCallback.CONNECT_STATUS_FAILURE_UNKNOWN;
23 
24 import android.net.Uri;
25 import android.net.wifi.ScanResult;
26 import android.net.wifi.WifiConfiguration;
27 import android.net.wifi.WifiInfo;
28 import android.net.wifi.WifiManager;
29 import android.net.wifi.hotspot2.OsuProvider;
30 import android.net.wifi.hotspot2.PasspointConfiguration;
31 import android.net.wifi.hotspot2.ProvisioningCallback;
32 import android.os.Handler;
33 import android.os.UserManager;
34 import android.text.TextUtils;
35 import android.util.Pair;
36 
37 import androidx.annotation.MainThread;
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 import androidx.annotation.WorkerThread;
41 import androidx.core.os.BuildCompat;
42 
43 import java.util.ArrayList;
44 import java.util.Collections;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.StringJoiner;
48 
49 /**
50  * WifiEntry representation of an Online Sign-up entry, uniquely identified by FQDN.
51  */
52 class OsuWifiEntry extends WifiEntry {
53     static final String KEY_PREFIX = "OsuWifiEntry:";
54 
55     // Scan result list must be thread safe for generating the verbose scan summary
56     @NonNull private final List<ScanResult> mCurrentScanResults = new ArrayList<>();
57 
58     @NonNull private final String mKey;
59     @NonNull private final OsuProvider mOsuProvider;
60     private String mSsid;
61     private String mOsuStatusString;
62     private boolean mIsAlreadyProvisioned = false;
63     private boolean mHasAddConfigUserRestriction = false;
64     private final UserManager mUserManager;
65 
66     /**
67      * Create an OsuWifiEntry with the associated OsuProvider
68      */
OsuWifiEntry( @onNull WifiTrackerInjector injector, @NonNull Handler callbackHandler, @NonNull OsuProvider osuProvider, @NonNull WifiManager wifiManager, boolean forSavedNetworksPage)69     OsuWifiEntry(
70             @NonNull WifiTrackerInjector injector,
71             @NonNull Handler callbackHandler,
72             @NonNull OsuProvider osuProvider,
73             @NonNull WifiManager wifiManager,
74             boolean forSavedNetworksPage) throws IllegalArgumentException {
75         super(injector, callbackHandler, wifiManager, forSavedNetworksPage);
76 
77         checkNotNull(osuProvider, "Cannot construct with null osuProvider!");
78 
79         mOsuProvider = osuProvider;
80         mKey = osuProviderToOsuWifiEntryKey(osuProvider);
81         mUserManager = injector.getUserManager();
82         if (BuildCompat.isAtLeastT() && mUserManager != null) {
83             mHasAddConfigUserRestriction = mUserManager.hasUserRestriction(
84                     UserManager.DISALLOW_ADD_WIFI_CONFIG);
85         }
86     }
87 
88     @Override
getKey()89     public String getKey() {
90         return mKey;
91     }
92 
93     @Override
getTitle()94     public synchronized String getTitle() {
95         final String friendlyName = mOsuProvider.getFriendlyName();
96         if (!TextUtils.isEmpty(friendlyName)) {
97             return friendlyName;
98         }
99         if (!TextUtils.isEmpty(mSsid)) {
100             return mSsid;
101         }
102         final Uri serverUri = mOsuProvider.getServerUri();
103         if (serverUri != null) {
104             return serverUri.toString();
105         }
106         return "";
107     }
108 
109     @Override
getSummary(boolean concise)110     public synchronized String getSummary(boolean concise) {
111         if (hasAdminRestrictions()) {
112             return mContext.getString(R.string.wifitrackerlib_admin_restricted_network);
113         }
114 
115         // TODO(b/70983952): Add verbose summary
116         if (mOsuStatusString != null) {
117             return mOsuStatusString;
118         } else if (isAlreadyProvisioned()) {
119             return concise ? mContext.getString(R.string.wifitrackerlib_wifi_passpoint_expired)
120                     : mContext.getString(
121                     R.string.wifitrackerlib_tap_to_renew_subscription_and_connect);
122         } else {
123             return mContext.getString(R.string.wifitrackerlib_tap_to_sign_up);
124         }
125     }
126 
127     @Override
getSsid()128     public synchronized String getSsid() {
129         return mSsid;
130     }
131 
132     @Override
getMacAddress()133     public String getMacAddress() {
134         // TODO(b/70983952): Fill this method in in case we need the mac address for verbose logging
135         return null;
136     }
137 
138     @Override
canConnect()139     public synchronized boolean canConnect() {
140         //check user restriction and whether the network is already provisioned
141         if (hasAdminRestrictions()) {
142             return false;
143         }
144         return mLevel != WIFI_LEVEL_UNREACHABLE
145                 && getConnectedState() == CONNECTED_STATE_DISCONNECTED;
146     }
147 
148     @Override
connect(@ullable ConnectCallback callback)149     public synchronized void connect(@Nullable ConnectCallback callback) {
150         mConnectCallback = callback;
151         mWifiManager.stopRestrictingAutoJoinToSubscriptionId();
152         mWifiManager.startSubscriptionProvisioning(mOsuProvider, mContext.getMainExecutor(),
153                 new OsuWifiEntryProvisioningCallback());
154     }
155 
156     @WorkerThread
updateScanResultInfo(@ullable List<ScanResult> scanResults)157     synchronized void updateScanResultInfo(@Nullable List<ScanResult> scanResults)
158             throws IllegalArgumentException {
159         if (scanResults == null) scanResults = new ArrayList<>();
160 
161         mCurrentScanResults.clear();
162         mCurrentScanResults.addAll(scanResults);
163 
164         final ScanResult bestScanResult = getBestScanResultByLevel(scanResults);
165         if (bestScanResult != null) {
166             mSsid = bestScanResult.SSID;
167             if (getConnectedState() == CONNECTED_STATE_DISCONNECTED) {
168                 mLevel = mWifiManager.calculateSignalLevel(bestScanResult.level);
169             }
170         } else {
171             mLevel = WIFI_LEVEL_UNREACHABLE;
172         }
173         notifyOnUpdated();
174     }
175 
176     @NonNull
osuProviderToOsuWifiEntryKey(@onNull OsuProvider osuProvider)177     static String osuProviderToOsuWifiEntryKey(@NonNull OsuProvider osuProvider) {
178         checkNotNull(osuProvider, "Cannot create key with null OsuProvider!");
179         return KEY_PREFIX + osuProvider.getFriendlyName() + ","
180                 + osuProvider.getServerUri().toString();
181     }
182 
183     @WorkerThread
184     @Override
connectionInfoMatches(@onNull WifiInfo wifiInfo)185     protected boolean connectionInfoMatches(@NonNull WifiInfo wifiInfo) {
186         return wifiInfo.isOsuAp() && TextUtils.equals(
187                 wifiInfo.getPasspointProviderFriendlyName(), mOsuProvider.getFriendlyName());
188     }
189 
190     @Override
getScanResultDescription()191     protected String getScanResultDescription() {
192         // TODO(b/70983952): Fill this method in.
193         return "";
194     }
195 
getOsuProvider()196     OsuProvider getOsuProvider() {
197         return mOsuProvider;
198     }
199 
isAlreadyProvisioned()200     synchronized boolean isAlreadyProvisioned() {
201         return mIsAlreadyProvisioned;
202     }
203 
setAlreadyProvisioned(boolean isAlreadyProvisioned)204     synchronized void setAlreadyProvisioned(boolean isAlreadyProvisioned) {
205         mIsAlreadyProvisioned = isAlreadyProvisioned;
206     }
207 
208     @Override
hasAdminRestrictions()209     public synchronized boolean hasAdminRestrictions() {
210         if (mHasAddConfigUserRestriction && !mIsAlreadyProvisioned) {
211             return true;
212         }
213         return false;
214     }
215 
216     class OsuWifiEntryProvisioningCallback extends ProvisioningCallback {
217         @Override
onProvisioningFailure(int status)218         @MainThread public void onProvisioningFailure(int status) {
219             synchronized (OsuWifiEntry.this) {
220                 if (TextUtils.equals(
221                         mOsuStatusString, mContext.getString(
222                                 R.string.wifitrackerlib_osu_completing_sign_up))) {
223                     mOsuStatusString =
224                             mContext.getString(R.string.wifitrackerlib_osu_sign_up_failed);
225                 } else {
226                     mOsuStatusString =
227                             mContext.getString(R.string.wifitrackerlib_osu_connect_failed);
228                 }
229             }
230             final ConnectCallback connectCallback = mConnectCallback;
231             if (connectCallback != null) {
232                 connectCallback.onConnectResult(CONNECT_STATUS_FAILURE_UNKNOWN);
233             }
234             notifyOnUpdated();
235         }
236 
237         @Override
onProvisioningStatus(int status)238         @MainThread public void onProvisioningStatus(int status) {
239             String newStatusString = null;
240             switch (status) {
241                 case OSU_STATUS_AP_CONNECTING:
242                 case OSU_STATUS_AP_CONNECTED:
243                 case OSU_STATUS_SERVER_CONNECTING:
244                 case OSU_STATUS_SERVER_VALIDATED:
245                 case OSU_STATUS_SERVER_CONNECTED:
246                 case OSU_STATUS_INIT_SOAP_EXCHANGE:
247                 case OSU_STATUS_WAITING_FOR_REDIRECT_RESPONSE:
248                     newStatusString = String.format(mContext.getString(
249                             R.string.wifitrackerlib_osu_opening_provider),
250                             getTitle());
251                     break;
252                 case OSU_STATUS_REDIRECT_RESPONSE_RECEIVED:
253                 case OSU_STATUS_SECOND_SOAP_EXCHANGE:
254                 case OSU_STATUS_THIRD_SOAP_EXCHANGE:
255                 case OSU_STATUS_RETRIEVING_TRUST_ROOT_CERTS:
256                     newStatusString = mContext.getString(
257                     R.string.wifitrackerlib_osu_completing_sign_up);
258                     break;
259             }
260             synchronized (OsuWifiEntry.this) {
261                 boolean updated = !TextUtils.equals(mOsuStatusString, newStatusString);
262                 mOsuStatusString = newStatusString;
263                 if (updated) {
264                     notifyOnUpdated();
265                 }
266             }
267         }
268 
269         @Override
onProvisioningComplete()270         @MainThread public void onProvisioningComplete() {
271             synchronized (OsuWifiEntry.this) {
272                 mOsuStatusString = mContext.getString(R.string.wifitrackerlib_osu_sign_up_complete);
273             }
274             notifyOnUpdated();
275 
276             PasspointConfiguration passpointConfig = mWifiManager
277                     .getMatchingPasspointConfigsForOsuProviders(Collections.singleton(mOsuProvider))
278                     .get(mOsuProvider);
279             final ConnectCallback connectCallback = mConnectCallback;
280             if (passpointConfig == null) {
281                 // Failed to find the config we just provisioned
282                 if (connectCallback != null) {
283                     connectCallback.onConnectResult(CONNECT_STATUS_FAILURE_UNKNOWN);
284                 }
285                 return;
286             }
287             String uniqueId = passpointConfig.getUniqueId();
288             for (Pair<WifiConfiguration, Map<Integer, List<ScanResult>>> pairing :
289                     mWifiManager.getAllMatchingWifiConfigs(mWifiManager.getScanResults())) {
290                 WifiConfiguration config = pairing.first;
291                 if (TextUtils.equals(config.getKey(), uniqueId)) {
292                     List<ScanResult> homeScans =
293                             pairing.second.get(WifiManager.PASSPOINT_HOME_NETWORK);
294                     List<ScanResult> roamingScans =
295                             pairing.second.get(WifiManager.PASSPOINT_ROAMING_NETWORK);
296                     ScanResult bestScan;
297                     if (homeScans != null && !homeScans.isEmpty()) {
298                         bestScan = getBestScanResultByLevel(homeScans);
299                     } else if (roamingScans != null && !roamingScans.isEmpty()) {
300                         bestScan = getBestScanResultByLevel(roamingScans);
301                     } else {
302                         break;
303                     }
304                     config.SSID = "\"" + bestScan.SSID + "\"";
305                     mWifiManager.connect(config, null /* ActionListener */);
306                     return;
307                 }
308             }
309 
310             // Failed to find the network we provisioned for
311             if (connectCallback != null) {
312                 connectCallback.onConnectResult(CONNECT_STATUS_FAILURE_UNKNOWN);
313             }
314         }
315     }
316 
317     @Override
toString()318     public String toString() {
319         StringJoiner sj = new StringJoiner("][", "[", "]");
320         sj.add("FriendlyName:" + mOsuProvider.getFriendlyName());
321         sj.add("ServerUri:" + mOsuProvider.getServerUri());
322         sj.add("SSID:" + mSsid);
323         return super.toString() + sj;
324     }
325 }
326