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