| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * 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 |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * 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. |
| */ |
| |
| package com.android.wifitrackerlib; |
| |
| import static androidx.core.util.Preconditions.checkNotNull; |
| |
| import static com.android.wifitrackerlib.Utils.getBestScanResultByLevel; |
| import static com.android.wifitrackerlib.WifiEntry.ConnectCallback.CONNECT_STATUS_FAILURE_UNKNOWN; |
| |
| import android.annotation.MainThread; |
| import android.content.Context; |
| import android.net.NetworkInfo; |
| import android.net.Uri; |
| import android.net.wifi.ScanResult; |
| import android.net.wifi.WifiConfiguration; |
| import android.net.wifi.WifiInfo; |
| import android.net.wifi.WifiManager; |
| import android.net.wifi.WifiNetworkScoreCache; |
| import android.net.wifi.hotspot2.OsuProvider; |
| import android.net.wifi.hotspot2.PasspointConfiguration; |
| import android.net.wifi.hotspot2.ProvisioningCallback; |
| import android.os.Handler; |
| import android.text.TextUtils; |
| import android.util.Pair; |
| |
| import androidx.annotation.GuardedBy; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.WorkerThread; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * WifiEntry representation of an Online Sign-up entry, uniquely identified by FQDN. |
| */ |
| class OsuWifiEntry extends WifiEntry { |
| static final String KEY_PREFIX = "OsuWifiEntry:"; |
| |
| private final Object mLock = new Object(); |
| // Scan result list must be thread safe for generating the verbose scan summary |
| @GuardedBy("mLock") |
| @NonNull private final List<ScanResult> mCurrentScanResults = new ArrayList<>(); |
| |
| @NonNull private final String mKey; |
| @NonNull private final Context mContext; |
| @NonNull private OsuProvider mOsuProvider; |
| private String mSsid; |
| private String mOsuStatusString; |
| private boolean mIsAlreadyProvisioned = false; |
| |
| /** |
| * Create an OsuWifiEntry with the associated OsuProvider |
| */ |
| OsuWifiEntry(@NonNull Context context, @NonNull Handler callbackHandler, |
| @NonNull OsuProvider osuProvider, |
| @NonNull WifiManager wifiManager, |
| @NonNull WifiNetworkScoreCache scoreCache, |
| boolean forSavedNetworksPage) throws IllegalArgumentException { |
| super(callbackHandler, wifiManager, scoreCache, forSavedNetworksPage); |
| |
| checkNotNull(osuProvider, "Cannot construct with null osuProvider!"); |
| |
| mContext = context; |
| mOsuProvider = osuProvider; |
| mKey = osuProviderToOsuWifiEntryKey(osuProvider); |
| } |
| |
| @Override |
| public String getKey() { |
| return mKey; |
| } |
| |
| @Override |
| public String getTitle() { |
| final String friendlyName = mOsuProvider.getFriendlyName(); |
| if (!TextUtils.isEmpty(friendlyName)) { |
| return friendlyName; |
| } |
| if (!TextUtils.isEmpty(mSsid)) { |
| return mSsid; |
| } |
| final Uri serverUri = mOsuProvider.getServerUri(); |
| if (serverUri != null) { |
| return serverUri.toString(); |
| } |
| return ""; |
| } |
| |
| @Override |
| public String getSummary(boolean concise) { |
| // TODO(b/70983952): Add verbose summary |
| if (mOsuStatusString != null) { |
| return mOsuStatusString; |
| } else if (isAlreadyProvisioned()) { |
| return concise ? mContext.getString(R.string.wifi_passpoint_expired) |
| : mContext.getString(R.string.tap_to_renew_subscription_and_connect); |
| } else { |
| return mContext.getString(R.string.tap_to_sign_up); |
| } |
| } |
| |
| @Override |
| public String getSsid() { |
| return mSsid; |
| } |
| |
| @Override |
| @Security |
| public int getSecurity() { |
| return SECURITY_NONE; |
| } |
| |
| @Override |
| public String getMacAddress() { |
| // TODO(b/70983952): Fill this method in in case we need the mac address for verbose logging |
| return null; |
| } |
| |
| @Override |
| public boolean isMetered() { |
| return false; |
| } |
| |
| @Override |
| public boolean isSaved() { |
| return false; |
| } |
| |
| @Override |
| public boolean isSuggestion() { |
| return false; |
| } |
| |
| @Override |
| public boolean isSubscription() { |
| return false; |
| } |
| |
| @Override |
| public WifiConfiguration getWifiConfiguration() { |
| return null; |
| } |
| |
| @Override |
| public boolean canConnect() { |
| return mLevel != WIFI_LEVEL_UNREACHABLE |
| && getConnectedState() == CONNECTED_STATE_DISCONNECTED; |
| } |
| |
| @Override |
| public void connect(@Nullable ConnectCallback callback) { |
| mConnectCallback = callback; |
| mWifiManager.startSubscriptionProvisioning(mOsuProvider, mContext.getMainExecutor(), |
| new OsuWifiEntryProvisioningCallback()); |
| } |
| |
| // Exiting from the OSU flow should disconnect from the network. |
| @Override |
| public boolean canDisconnect() { |
| return false; |
| } |
| |
| @Override |
| public void disconnect(@Nullable DisconnectCallback callback) { |
| } |
| |
| @Override |
| public boolean canForget() { |
| return false; |
| } |
| |
| @Override |
| public void forget(@Nullable ForgetCallback callback) { |
| } |
| |
| @Override |
| public boolean canSignIn() { |
| return false; |
| } |
| |
| @Override |
| public void signIn(@Nullable SignInCallback callback) { |
| return; |
| } |
| |
| @Override |
| public boolean canShare() { |
| return false; |
| } |
| |
| @Override |
| public boolean canEasyConnect() { |
| return false; |
| } |
| |
| @Override |
| @MeteredChoice |
| public int getMeteredChoice() { |
| // Metered choice is meaningless for OSU entries |
| return METERED_CHOICE_AUTO; |
| } |
| |
| @Override |
| public boolean canSetMeteredChoice() { |
| return false; |
| } |
| |
| @Override |
| public void setMeteredChoice(int meteredChoice) { |
| // Metered choice is meaningless for OSU entries |
| } |
| |
| @Override |
| @Privacy |
| public int getPrivacy() { |
| // MAC Randomization choice is meaningless for OSU entries. |
| return PRIVACY_UNKNOWN; |
| } |
| |
| @Override |
| public boolean canSetPrivacy() { |
| return false; |
| } |
| |
| @Override |
| public void setPrivacy(int privacy) { |
| // MAC Randomization choice is meaningless for OSU entries. |
| } |
| |
| @Override |
| public boolean isAutoJoinEnabled() { |
| return false; |
| } |
| |
| @Override |
| public boolean canSetAutoJoinEnabled() { |
| return false; |
| } |
| |
| @Override |
| public void setAutoJoinEnabled(boolean enabled) { |
| } |
| |
| @Override |
| public String getSecurityString(boolean concise) { |
| return ""; |
| } |
| |
| @Override |
| public boolean isExpired() { |
| return false; |
| } |
| |
| @WorkerThread |
| void updateScanResultInfo(@Nullable List<ScanResult> scanResults) |
| throws IllegalArgumentException { |
| if (scanResults == null) scanResults = new ArrayList<>(); |
| |
| synchronized (mLock) { |
| mCurrentScanResults.clear(); |
| mCurrentScanResults.addAll(scanResults); |
| } |
| |
| final ScanResult bestScanResult = getBestScanResultByLevel(scanResults); |
| if (bestScanResult != null) { |
| mSsid = bestScanResult.SSID; |
| if (getConnectedState() == CONNECTED_STATE_DISCONNECTED) { |
| mLevel = mWifiManager.calculateSignalLevel(bestScanResult.level); |
| } |
| } else { |
| mLevel = WIFI_LEVEL_UNREACHABLE; |
| } |
| notifyOnUpdated(); |
| } |
| |
| @NonNull |
| static String osuProviderToOsuWifiEntryKey(@NonNull OsuProvider osuProvider) { |
| checkNotNull(osuProvider, "Cannot create key with null OsuProvider!"); |
| return KEY_PREFIX + osuProvider.getFriendlyName() + "," |
| + osuProvider.getServerUri().toString(); |
| } |
| |
| @WorkerThread |
| @Override |
| protected boolean connectionInfoMatches(@NonNull WifiInfo wifiInfo, |
| @NonNull NetworkInfo networkInfo) { |
| return wifiInfo.isOsuAp() && TextUtils.equals( |
| wifiInfo.getPasspointProviderFriendlyName(), mOsuProvider.getFriendlyName()); |
| } |
| |
| @Override |
| String getScanResultDescription() { |
| // TODO(b/70983952): Fill this method in. |
| return ""; |
| } |
| |
| OsuProvider getOsuProvider() { |
| return mOsuProvider; |
| } |
| |
| boolean isAlreadyProvisioned() { |
| return mIsAlreadyProvisioned; |
| } |
| |
| void setAlreadyProvisioned(boolean isAlreadyProvisioned) { |
| mIsAlreadyProvisioned = isAlreadyProvisioned; |
| } |
| |
| class OsuWifiEntryProvisioningCallback extends ProvisioningCallback { |
| @Override |
| @MainThread public void onProvisioningFailure(int status) { |
| if (TextUtils.equals( |
| mOsuStatusString, mContext.getString(R.string.osu_completing_sign_up))) { |
| mOsuStatusString = mContext.getString(R.string.osu_sign_up_failed); |
| } else { |
| mOsuStatusString = mContext.getString(R.string.osu_connect_failed); |
| } |
| if (mConnectCallback != null) { |
| mConnectCallback.onConnectResult(CONNECT_STATUS_FAILURE_UNKNOWN); |
| } |
| notifyOnUpdated(); |
| } |
| |
| @Override |
| @MainThread public void onProvisioningStatus(int status) { |
| String newStatusString = null; |
| switch (status) { |
| case OSU_STATUS_AP_CONNECTING: |
| case OSU_STATUS_AP_CONNECTED: |
| case OSU_STATUS_SERVER_CONNECTING: |
| case OSU_STATUS_SERVER_VALIDATED: |
| case OSU_STATUS_SERVER_CONNECTED: |
| case OSU_STATUS_INIT_SOAP_EXCHANGE: |
| case OSU_STATUS_WAITING_FOR_REDIRECT_RESPONSE: |
| newStatusString = String.format(mContext.getString( |
| R.string.osu_opening_provider), |
| getTitle()); |
| break; |
| case OSU_STATUS_REDIRECT_RESPONSE_RECEIVED: |
| case OSU_STATUS_SECOND_SOAP_EXCHANGE: |
| case OSU_STATUS_THIRD_SOAP_EXCHANGE: |
| case OSU_STATUS_RETRIEVING_TRUST_ROOT_CERTS: |
| newStatusString = mContext.getString(R.string.osu_completing_sign_up); |
| break; |
| } |
| boolean updated = !TextUtils.equals(mOsuStatusString, newStatusString); |
| mOsuStatusString = newStatusString; |
| if (updated) { |
| notifyOnUpdated(); |
| } |
| } |
| |
| @Override |
| @MainThread public void onProvisioningComplete() { |
| mOsuStatusString = mContext.getString(R.string.osu_sign_up_complete); |
| notifyOnUpdated(); |
| |
| PasspointConfiguration passpointConfig = mWifiManager |
| .getMatchingPasspointConfigsForOsuProviders(Collections.singleton(mOsuProvider)) |
| .get(mOsuProvider); |
| if (passpointConfig == null) { |
| // Failed to find the config we just provisioned |
| if (mConnectCallback != null) { |
| mConnectCallback.onConnectResult(CONNECT_STATUS_FAILURE_UNKNOWN); |
| } |
| return; |
| } |
| String uniqueId = passpointConfig.getUniqueId(); |
| for (Pair<WifiConfiguration, Map<Integer, List<ScanResult>>> pairing : |
| mWifiManager.getAllMatchingWifiConfigs(mWifiManager.getScanResults())) { |
| WifiConfiguration config = pairing.first; |
| if (TextUtils.equals(config.getKey(), uniqueId)) { |
| List<ScanResult> homeScans = |
| pairing.second.get(WifiManager.PASSPOINT_HOME_NETWORK); |
| List<ScanResult> roamingScans = |
| pairing.second.get(WifiManager.PASSPOINT_ROAMING_NETWORK); |
| ScanResult bestScan; |
| if (homeScans != null && !homeScans.isEmpty()) { |
| bestScan = getBestScanResultByLevel(homeScans); |
| } else if (roamingScans != null && !roamingScans.isEmpty()) { |
| bestScan = getBestScanResultByLevel(roamingScans); |
| } else { |
| break; |
| } |
| config.SSID = "\"" + bestScan.SSID + "\""; |
| mWifiManager.connect(config, null /* ActionListener */); |
| return; |
| } |
| } |
| |
| // Failed to find the network we provisioned for |
| if (mConnectCallback != null) { |
| mConnectCallback.onConnectResult(CONNECT_STATUS_FAILURE_UNKNOWN); |
| } |
| } |
| } |
| } |