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