blob: 6a5db5d2304096372d8316f18a72c10700e3ff32 [file] [log] [blame]
/*
* Copyright (C) 2018 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.server.wifi;
import static android.app.AppOpsManager.MODE_IGNORED;
import static android.app.AppOpsManager.OPSTR_CHANGE_WIFI_STATE;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.MacAddress;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiNetworkSuggestion;
import android.os.Handler;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.server.wifi.util.WifiPermissionsUtil;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.concurrent.NotThreadSafe;
/**
* Network Suggestions Manager.
* NOTE: This class should always be invoked from the main wifi service thread.
*/
@NotThreadSafe
public class WifiNetworkSuggestionsManager {
private static final String TAG = "WifiNetworkSuggestionsManager";
/** Intent when user tapped action button to allow the app. */
@VisibleForTesting
public static final String NOTIFICATION_USER_ALLOWED_APP_INTENT_ACTION =
"com.android.server.wifi.action.NetworkSuggestion.USER_ALLOWED_APP";
/** Intent when user tapped action button to disallow the app. */
@VisibleForTesting
public static final String NOTIFICATION_USER_DISALLOWED_APP_INTENT_ACTION =
"com.android.server.wifi.action.NetworkSuggestion.USER_DISALLOWED_APP";
/** Intent when user dismissed the notification. */
@VisibleForTesting
public static final String NOTIFICATION_USER_DISMISSED_INTENT_ACTION =
"com.android.server.wifi.action.NetworkSuggestion.USER_DISMISSED";
@VisibleForTesting
public static final String EXTRA_PACKAGE_NAME =
"com.android.server.wifi.extra.NetworkSuggestion.PACKAGE_NAME";
@VisibleForTesting
public static final String EXTRA_UID =
"com.android.server.wifi.extra.NetworkSuggestion.UID";
private final Context mContext;
private final Resources mResources;
private final Handler mHandler;
private final AppOpsManager mAppOps;
private final NotificationManager mNotificationManager;
private final PackageManager mPackageManager;
private final WifiPermissionsUtil mWifiPermissionsUtil;
private final WifiConfigManager mWifiConfigManager;
private final WifiMetrics mWifiMetrics;
private final WifiInjector mWifiInjector;
private final FrameworkFacade mFrameworkFacade;
/**
* Per app meta data to store network suggestions, status, etc for each app providing network
* suggestions on the device.
*/
public static class PerAppInfo {
/**
* Package Name of the app.
*/
public final String packageName;
/**
* Set of active network suggestions provided by the app.
*/
public final Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions = new HashSet<>();
/**
* Whether we have shown the user a notification for this app.
*/
public boolean hasUserApproved = false;
/** Stores the max size of the {@link #extNetworkSuggestions} list ever for this app */
public int maxSize = 0;
public PerAppInfo(@NonNull String packageName) {
this.packageName = packageName;
}
// This is only needed for comparison in unit tests.
@Override
public boolean equals(Object other) {
if (other == null) return false;
if (!(other instanceof PerAppInfo)) return false;
PerAppInfo otherPerAppInfo = (PerAppInfo) other;
return TextUtils.equals(packageName, otherPerAppInfo.packageName)
&& Objects.equals(extNetworkSuggestions, otherPerAppInfo.extNetworkSuggestions)
&& hasUserApproved == otherPerAppInfo.hasUserApproved;
}
// This is only needed for comparison in unit tests.
@Override
public int hashCode() {
return Objects.hash(packageName, extNetworkSuggestions, hasUserApproved);
}
}
/**
* Internal container class which holds a network suggestion and a pointer to the
* {@link PerAppInfo} entry from {@link #mActiveNetworkSuggestionsPerApp} corresponding to the
* app that made the suggestion.
*/
public static class ExtendedWifiNetworkSuggestion {
public final WifiNetworkSuggestion wns;
// Store the pointer to the corresponding app's meta data.
public final PerAppInfo perAppInfo;
public ExtendedWifiNetworkSuggestion(@NonNull WifiNetworkSuggestion wns,
@NonNull PerAppInfo perAppInfo) {
this.wns = wns;
this.perAppInfo = perAppInfo;
}
@Override
public int hashCode() {
return Objects.hash(wns); // perAppInfo not used for equals.
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof ExtendedWifiNetworkSuggestion)) {
return false;
}
ExtendedWifiNetworkSuggestion other = (ExtendedWifiNetworkSuggestion) obj;
return wns.equals(other.wns); // perAppInfo not used for equals.
}
@Override
public String toString() {
return "Extended" + wns.toString();
}
/**
* Convert from {@link WifiNetworkSuggestion} to a new instance of
* {@link ExtendedWifiNetworkSuggestion}.
*/
public static ExtendedWifiNetworkSuggestion fromWns(
@NonNull WifiNetworkSuggestion wns, @NonNull PerAppInfo perAppInfo) {
return new ExtendedWifiNetworkSuggestion(wns, perAppInfo);
}
}
/**
* Map of package name of an app to the set of active network suggestions provided by the app.
*/
private final Map<String, PerAppInfo> mActiveNetworkSuggestionsPerApp = new HashMap<>();
/**
* Map of package name of an app to the app ops changed listener for the app.
*/
private final Map<String, AppOpsChangedListener> mAppOpsChangedListenerPerApp = new HashMap<>();
/**
* Map maintained to help lookup all the network suggestions (with no bssid) that match a
* provided scan result.
* Note:
* <li>There could be multiple suggestions (provided by different apps) that match a single
* scan result.</li>
* <li>Adding/Removing to this set for scan result lookup is expensive. But, we expect scan
* result lookup to happen much more often than apps modifying network suggestions.</li>
*/
private final Map<ScanResultMatchInfo, Set<ExtendedWifiNetworkSuggestion>>
mActiveScanResultMatchInfoWithNoBssid = new HashMap<>();
/**
* Map maintained to help lookup all the network suggestions (with bssid) that match a provided
* scan result.
* Note:
* <li>There could be multiple suggestions (provided by different apps) that match a single
* scan result.</li>
* <li>Adding/Removing to this set for scan result lookup is expensive. But, we expect scan
* result lookup to happen much more often than apps modifying network suggestions.</li>
*/
private final Map<Pair<ScanResultMatchInfo, MacAddress>, Set<ExtendedWifiNetworkSuggestion>>
mActiveScanResultMatchInfoWithBssid = new HashMap<>();
/**
* List of {@link WifiNetworkSuggestion} matching the current connected network.
*/
private Set<ExtendedWifiNetworkSuggestion> mActiveNetworkSuggestionsMatchingConnection;
/**
* Intent filter for processing notification actions.
*/
private final IntentFilter mIntentFilter;
/**
* Verbose logging flag.
*/
private boolean mVerboseLoggingEnabled = false;
/**
* Indicates that we have new data to serialize.
*/
private boolean mHasNewDataToSerialize = false;
/**
* Indicates if the user approval notification is active.
*/
private boolean mUserApprovalNotificationActive = false;
/**
* Stores the name of the user approval notification that is active.
*/
private String mUserApprovalNotificationPackageName;
/**
* Listener for app-ops changes for active suggestor apps.
*/
private final class AppOpsChangedListener implements AppOpsManager.OnOpChangedListener {
private final String mPackageName;
private final int mUid;
AppOpsChangedListener(@NonNull String packageName, int uid) {
mPackageName = packageName;
mUid = uid;
}
@Override
public void onOpChanged(String op, String packageName) {
mHandler.post(() -> {
if (!mPackageName.equals(packageName)) return;
if (!OPSTR_CHANGE_WIFI_STATE.equals(op)) return;
// Ensure the uid to package mapping is still correct.
try {
mAppOps.checkPackage(mUid, mPackageName);
} catch (SecurityException e) {
Log.wtf(TAG, "Invalid uid/package" + packageName);
return;
}
if (mAppOps.unsafeCheckOpNoThrow(OPSTR_CHANGE_WIFI_STATE, mUid, mPackageName)
== AppOpsManager.MODE_IGNORED) {
Log.i(TAG, "User disallowed change wifi state for " + packageName);
// User disabled the app, remove app from database. We want the notification
// again if the user enabled the app-op back.
removeApp(mPackageName);
}
});
}
};
/**
* Module to interact with the wifi config store.
*/
private class NetworkSuggestionDataSource implements NetworkSuggestionStoreData.DataSource {
@Override
public Map<String, PerAppInfo> toSerialize() {
// Clear the flag after writing to disk.
// TODO(b/115504887): Don't reset the flag on write failure.
mHasNewDataToSerialize = false;
return mActiveNetworkSuggestionsPerApp;
}
@Override
public void fromDeserialized(Map<String, PerAppInfo> networkSuggestionsMap) {
mActiveNetworkSuggestionsPerApp.putAll(networkSuggestionsMap);
// Build the scan cache.
for (Map.Entry<String, PerAppInfo> entry : networkSuggestionsMap.entrySet()) {
String packageName = entry.getKey();
Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions =
entry.getValue().extNetworkSuggestions;
if (!extNetworkSuggestions.isEmpty()) {
// Start tracking app-op changes from the app if they have active suggestions.
startTrackingAppOpsChange(packageName,
extNetworkSuggestions.iterator().next().wns.suggestorUid);
}
addToScanResultMatchInfoMap(extNetworkSuggestions);
}
}
@Override
public void reset() {
mActiveNetworkSuggestionsPerApp.clear();
mActiveScanResultMatchInfoWithBssid.clear();
mActiveScanResultMatchInfoWithNoBssid.clear();
}
@Override
public boolean hasNewDataToSerialize() {
return mHasNewDataToSerialize;
}
}
private final BroadcastReceiver mBroadcastReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
if (packageName == null) {
Log.e(TAG, "No package name found in intent");
return;
}
int uid = intent.getIntExtra(EXTRA_UID, -1);
if (uid == -1) {
Log.e(TAG, "No uid found in intent");
return;
}
switch (intent.getAction()) {
case NOTIFICATION_USER_ALLOWED_APP_INTENT_ACTION:
Log.i(TAG, "User clicked to allow app");
// Set the user approved flag.
setHasUserApprovedForApp(true, packageName);
break;
case NOTIFICATION_USER_DISALLOWED_APP_INTENT_ACTION:
Log.i(TAG, "User clicked to disallow app");
// Set the user approved flag.
setHasUserApprovedForApp(false, packageName);
// Take away CHANGE_WIFI_STATE app-ops from the app.
mAppOps.setMode(AppOpsManager.OP_CHANGE_WIFI_STATE, uid, packageName,
MODE_IGNORED);
break;
case NOTIFICATION_USER_DISMISSED_INTENT_ACTION:
Log.i(TAG, "User dismissed the notification");
mUserApprovalNotificationActive = false;
return; // no need to cancel a dismissed notification, return.
default:
Log.e(TAG, "Unknown action " + intent.getAction());
return;
}
// Clear notification once the user interacts with it.
mUserApprovalNotificationActive = false;
mNotificationManager.cancel(SystemMessage.NOTE_NETWORK_SUGGESTION_AVAILABLE);
}
};
public WifiNetworkSuggestionsManager(Context context, Handler handler,
WifiInjector wifiInjector,
WifiPermissionsUtil wifiPermissionsUtil,
WifiConfigManager wifiConfigManager,
WifiConfigStore wifiConfigStore,
WifiMetrics wifiMetrics) {
mContext = context;
mResources = context.getResources();
mHandler = handler;
mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
mNotificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
mPackageManager = context.getPackageManager();
mWifiInjector = wifiInjector;
mFrameworkFacade = mWifiInjector.getFrameworkFacade();
mWifiPermissionsUtil = wifiPermissionsUtil;
mWifiConfigManager = wifiConfigManager;
mWifiMetrics = wifiMetrics;
// register the data store for serializing/deserializing data.
wifiConfigStore.registerStoreData(
wifiInjector.makeNetworkSuggestionStoreData(new NetworkSuggestionDataSource()));
// Register broadcast receiver for UI interactions.
mIntentFilter = new IntentFilter();
mIntentFilter.addAction(NOTIFICATION_USER_ALLOWED_APP_INTENT_ACTION);
mIntentFilter.addAction(NOTIFICATION_USER_DISALLOWED_APP_INTENT_ACTION);
mIntentFilter.addAction(NOTIFICATION_USER_DISMISSED_INTENT_ACTION);
mContext.registerReceiver(mBroadcastReceiver, mIntentFilter);
}
/**
* Enable verbose logging.
*/
public void enableVerboseLogging(int verbose) {
mVerboseLoggingEnabled = verbose > 0;
}
private void saveToStore() {
// Set the flag to let WifiConfigStore that we have new data to write.
mHasNewDataToSerialize = true;
if (!mWifiConfigManager.saveToStore(true)) {
Log.w(TAG, "Failed to save to store");
}
}
private void addToScanResultMatchInfoMap(
@NonNull Collection<ExtendedWifiNetworkSuggestion> extNetworkSuggestions) {
for (ExtendedWifiNetworkSuggestion extNetworkSuggestion : extNetworkSuggestions) {
ScanResultMatchInfo scanResultMatchInfo =
ScanResultMatchInfo.fromWifiConfiguration(
extNetworkSuggestion.wns.wifiConfiguration);
Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestionsForScanResultMatchInfo;
if (!TextUtils.isEmpty(extNetworkSuggestion.wns.wifiConfiguration.BSSID)) {
Pair<ScanResultMatchInfo, MacAddress> lookupPair =
Pair.create(scanResultMatchInfo,
MacAddress.fromString(
extNetworkSuggestion.wns.wifiConfiguration.BSSID));
extNetworkSuggestionsForScanResultMatchInfo =
mActiveScanResultMatchInfoWithBssid.get(lookupPair);
if (extNetworkSuggestionsForScanResultMatchInfo == null) {
extNetworkSuggestionsForScanResultMatchInfo = new HashSet<>();
mActiveScanResultMatchInfoWithBssid.put(
lookupPair, extNetworkSuggestionsForScanResultMatchInfo);
}
} else {
extNetworkSuggestionsForScanResultMatchInfo =
mActiveScanResultMatchInfoWithNoBssid.get(scanResultMatchInfo);
if (extNetworkSuggestionsForScanResultMatchInfo == null) {
extNetworkSuggestionsForScanResultMatchInfo = new HashSet<>();
mActiveScanResultMatchInfoWithNoBssid.put(
scanResultMatchInfo, extNetworkSuggestionsForScanResultMatchInfo);
}
}
extNetworkSuggestionsForScanResultMatchInfo.add(extNetworkSuggestion);
}
}
private void removeFromScanResultMatchInfoMap(
@NonNull Collection<ExtendedWifiNetworkSuggestion> extNetworkSuggestions) {
for (ExtendedWifiNetworkSuggestion extNetworkSuggestion : extNetworkSuggestions) {
ScanResultMatchInfo scanResultMatchInfo =
ScanResultMatchInfo.fromWifiConfiguration(
extNetworkSuggestion.wns.wifiConfiguration);
Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestionsForScanResultMatchInfo;
if (!TextUtils.isEmpty(extNetworkSuggestion.wns.wifiConfiguration.BSSID)) {
Pair<ScanResultMatchInfo, MacAddress> lookupPair =
Pair.create(scanResultMatchInfo,
MacAddress.fromString(
extNetworkSuggestion.wns.wifiConfiguration.BSSID));
extNetworkSuggestionsForScanResultMatchInfo =
mActiveScanResultMatchInfoWithBssid.get(lookupPair);
// This should never happen because we should have done necessary error checks in
// the parent method.
if (extNetworkSuggestionsForScanResultMatchInfo == null) {
Log.wtf(TAG, "No scan result match info found.");
}
extNetworkSuggestionsForScanResultMatchInfo.remove(extNetworkSuggestion);
// Remove the set from map if empty.
if (extNetworkSuggestionsForScanResultMatchInfo.isEmpty()) {
mActiveScanResultMatchInfoWithBssid.remove(lookupPair);
}
} else {
extNetworkSuggestionsForScanResultMatchInfo =
mActiveScanResultMatchInfoWithNoBssid.get(scanResultMatchInfo);
// This should never happen because we should have done necessary error checks in
// the parent method.
if (extNetworkSuggestionsForScanResultMatchInfo == null) {
Log.wtf(TAG, "No scan result match info found.");
}
extNetworkSuggestionsForScanResultMatchInfo.remove(extNetworkSuggestion);
// Remove the set from map if empty.
if (extNetworkSuggestionsForScanResultMatchInfo.isEmpty()) {
mActiveScanResultMatchInfoWithNoBssid.remove(scanResultMatchInfo);
}
}
}
}
// Issues a disconnect if the only serving network suggestion is removed.
// TODO (b/115504887): What if there is also a saved network with the same credentials?
private void triggerDisconnectIfServingNetworkSuggestionRemoved(
Collection<ExtendedWifiNetworkSuggestion> extNetworkSuggestionsRemoved) {
if (mActiveNetworkSuggestionsMatchingConnection == null
|| mActiveNetworkSuggestionsMatchingConnection.isEmpty()) {
return;
}
if (mActiveNetworkSuggestionsMatchingConnection.removeAll(extNetworkSuggestionsRemoved)) {
if (mActiveNetworkSuggestionsMatchingConnection.isEmpty()) {
Log.i(TAG, "Only network suggestion matching the connected network removed. "
+ "Disconnecting...");
mWifiInjector.getClientModeImpl().disconnectCommand();
}
}
}
private void startTrackingAppOpsChange(@NonNull String packageName, int uid) {
AppOpsChangedListener appOpsChangedListener =
new AppOpsChangedListener(packageName, uid);
mAppOps.startWatchingMode(OPSTR_CHANGE_WIFI_STATE, packageName, appOpsChangedListener);
mAppOpsChangedListenerPerApp.put(packageName, appOpsChangedListener);
}
/**
* Helper method to convert the incoming collection of public {@link WifiNetworkSuggestion}
* objects to a set of corresponding internal wrapper
* {@link ExtendedWifiNetworkSuggestion} objects.
*/
private Set<ExtendedWifiNetworkSuggestion> convertToExtendedWnsSet(
final Collection<WifiNetworkSuggestion> networkSuggestions,
final PerAppInfo perAppInfo) {
return networkSuggestions
.stream()
.collect(Collectors.mapping(
n -> ExtendedWifiNetworkSuggestion.fromWns(n, perAppInfo),
Collectors.toSet()));
}
/**
* Helper method to convert the incoming collection of internal wrapper
* {@link ExtendedWifiNetworkSuggestion} objects to a set of corresponding public
* {@link WifiNetworkSuggestion} objects.
*/
private Set<WifiNetworkSuggestion> convertToWnsSet(
final Collection<ExtendedWifiNetworkSuggestion> extNetworkSuggestions) {
return extNetworkSuggestions
.stream()
.collect(Collectors.mapping(
n -> n.wns,
Collectors.toSet()));
}
/**
* Add the provided list of network suggestions from the corresponding app's active list.
*/
public @WifiManager.NetworkSuggestionsStatusCode int add(
List<WifiNetworkSuggestion> networkSuggestions, int uid, String packageName) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Adding " + networkSuggestions.size() + " networks from " + packageName);
}
if (networkSuggestions.isEmpty()) {
Log.w(TAG, "Empty list of network suggestions for " + packageName + ". Ignoring");
return WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS;
}
PerAppInfo perAppInfo = mActiveNetworkSuggestionsPerApp.get(packageName);
if (perAppInfo == null) {
perAppInfo = new PerAppInfo(packageName);
mActiveNetworkSuggestionsPerApp.put(packageName, perAppInfo);
if (mWifiPermissionsUtil.checkNetworkCarrierProvisioningPermission(uid)) {
Log.i(TAG, "Setting the carrier provisioning app approved");
perAppInfo.hasUserApproved = true;
}
}
Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions =
convertToExtendedWnsSet(networkSuggestions, perAppInfo);
// check if the app is trying to in-place modify network suggestions.
if (!Collections.disjoint(perAppInfo.extNetworkSuggestions, extNetworkSuggestions)) {
Log.e(TAG, "Failed to add network suggestions for " + packageName
+ ". Modification of active network suggestions disallowed");
return WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_DUPLICATE;
}
if (perAppInfo.extNetworkSuggestions.size() + extNetworkSuggestions.size()
> WifiManager.NETWORK_SUGGESTIONS_MAX_PER_APP) {
Log.e(TAG, "Failed to add network suggestions for " + packageName
+ ". Exceeds max per app, current list size: "
+ perAppInfo.extNetworkSuggestions.size()
+ ", new list size: "
+ extNetworkSuggestions.size());
return WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_EXCEEDS_MAX_PER_APP;
}
if (perAppInfo.extNetworkSuggestions.isEmpty()) {
// Start tracking app-op changes from the app if they have active suggestions.
startTrackingAppOpsChange(packageName, uid);
}
perAppInfo.extNetworkSuggestions.addAll(extNetworkSuggestions);
// Update the max size for this app.
perAppInfo.maxSize = Math.max(perAppInfo.extNetworkSuggestions.size(), perAppInfo.maxSize);
addToScanResultMatchInfoMap(extNetworkSuggestions);
saveToStore();
mWifiMetrics.incrementNetworkSuggestionApiNumModification();
mWifiMetrics.noteNetworkSuggestionApiListSizeHistogram(getAllMaxSizes());
return WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS;
}
private void stopTrackingAppOpsChange(@NonNull String packageName) {
AppOpsChangedListener appOpsChangedListener =
mAppOpsChangedListenerPerApp.remove(packageName);
if (appOpsChangedListener == null) {
Log.wtf(TAG, "No app ops listener found for " + packageName);
return;
}
mAppOps.stopWatchingMode(appOpsChangedListener);
}
private void removeInternal(
@NonNull Collection<ExtendedWifiNetworkSuggestion> extNetworkSuggestions,
@NonNull String packageName,
@NonNull PerAppInfo perAppInfo) {
if (!extNetworkSuggestions.isEmpty()) {
perAppInfo.extNetworkSuggestions.removeAll(extNetworkSuggestions);
} else {
// empty list is used to clear everything for the app. Store a copy for use below.
extNetworkSuggestions = new HashSet<>(perAppInfo.extNetworkSuggestions);
perAppInfo.extNetworkSuggestions.clear();
}
if (perAppInfo.extNetworkSuggestions.isEmpty()) {
// Note: We don't remove the app entry even if there is no active suggestions because
// we want to keep the notification state for all apps that have ever provided
// suggestions.
if (mVerboseLoggingEnabled) Log.v(TAG, "No active suggestions for " + packageName);
// Stop tracking app-op changes from the app if they don't have active suggestions.
stopTrackingAppOpsChange(packageName);
}
// Clear the scan cache.
removeFromScanResultMatchInfoMap(extNetworkSuggestions);
}
/**
* Remove the provided list of network suggestions from the corresponding app's active list.
*/
public @WifiManager.NetworkSuggestionsStatusCode int remove(
List<WifiNetworkSuggestion> networkSuggestions, String packageName) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Removing " + networkSuggestions.size() + " networks from " + packageName);
}
PerAppInfo perAppInfo = mActiveNetworkSuggestionsPerApp.get(packageName);
if (perAppInfo == null) {
Log.e(TAG, "Failed to remove network suggestions for " + packageName
+ ". No network suggestions found");
return WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_REMOVE_INVALID;
}
Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions =
convertToExtendedWnsSet(networkSuggestions, perAppInfo);
// check if all the request network suggestions are present in the active list.
if (!extNetworkSuggestions.isEmpty()
&& !perAppInfo.extNetworkSuggestions.containsAll(extNetworkSuggestions)) {
Log.e(TAG, "Failed to remove network suggestions for " + packageName
+ ". Network suggestions not found in active network suggestions");
return WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_REMOVE_INVALID;
}
removeInternal(extNetworkSuggestions, packageName, perAppInfo);
saveToStore();
mWifiMetrics.incrementNetworkSuggestionApiNumModification();
mWifiMetrics.noteNetworkSuggestionApiListSizeHistogram(getAllMaxSizes());
return WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS;
}
/**
* Remove all tracking of the app that has been uninstalled.
*/
public void removeApp(@NonNull String packageName) {
PerAppInfo perAppInfo = mActiveNetworkSuggestionsPerApp.get(packageName);
if (perAppInfo == null) return;
// Disconnect from the current network, if the only suggestion for it was removed.
triggerDisconnectIfServingNetworkSuggestionRemoved(perAppInfo.extNetworkSuggestions);
removeInternal(Collections.EMPTY_LIST, packageName, perAppInfo);
// Remove the package fully from the internal database.
mActiveNetworkSuggestionsPerApp.remove(packageName);
saveToStore();
Log.i(TAG, "Removed " + packageName);
}
/**
* Clear all internal state (for network settings reset).
*/
public void clear() {
Iterator<Map.Entry<String, PerAppInfo>> iter =
mActiveNetworkSuggestionsPerApp.entrySet().iterator();
// Disconnect if we're connected to one of the suggestions.
triggerDisconnectIfServingNetworkSuggestionRemoved(
mActiveNetworkSuggestionsMatchingConnection);
while (iter.hasNext()) {
Map.Entry<String, PerAppInfo> entry = iter.next();
removeInternal(Collections.EMPTY_LIST, entry.getKey(), entry.getValue());
iter.remove();
}
saveToStore();
Log.i(TAG, "Cleared all internal state");
}
/**
* Check if network suggestions are enabled or disabled for the app.
*/
public boolean hasUserApprovedForApp(String packageName) {
PerAppInfo perAppInfo = mActiveNetworkSuggestionsPerApp.get(packageName);
if (perAppInfo == null) return false;
return perAppInfo.hasUserApproved;
}
/**
* Enable or Disable network suggestions for the app.
*/
public void setHasUserApprovedForApp(boolean approved, String packageName) {
PerAppInfo perAppInfo = mActiveNetworkSuggestionsPerApp.get(packageName);
if (perAppInfo == null) return;
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Setting the app " + (approved ? "approved" : "not approved"));
}
perAppInfo.hasUserApproved = approved;
saveToStore();
}
/**
* Returns a set of all network suggestions across all apps.
*/
@VisibleForTesting
public Set<WifiNetworkSuggestion> getAllNetworkSuggestions() {
return mActiveNetworkSuggestionsPerApp.values()
.stream()
.flatMap(e -> convertToWnsSet(e.extNetworkSuggestions)
.stream())
.collect(Collectors.toSet());
}
private List<Integer> getAllMaxSizes() {
return mActiveNetworkSuggestionsPerApp.values()
.stream()
.map(e -> e.maxSize)
.collect(Collectors.toList());
}
private PendingIntent getPrivateBroadcast(@NonNull String action, @NonNull String packageName,
int uid) {
Intent intent = new Intent(action)
.setPackage("android")
.putExtra(EXTRA_PACKAGE_NAME, packageName)
.putExtra(EXTRA_UID, uid);
return mFrameworkFacade.getBroadcast(mContext, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
private @NonNull CharSequence getAppName(@NonNull String packageName) {
ApplicationInfo applicationInfo = null;
try {
applicationInfo = mPackageManager.getApplicationInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Failed to find app name for " + packageName);
return "";
}
CharSequence appName = mPackageManager.getApplicationLabel(applicationInfo);
return (appName != null) ? appName : "";
}
private void sendUserApprovalNotification(@NonNull String packageName, int uid) {
Notification.Action userAllowAppNotificationAction =
new Notification.Action.Builder(null,
mResources.getText(R.string.wifi_suggestion_action_allow_app),
getPrivateBroadcast(NOTIFICATION_USER_ALLOWED_APP_INTENT_ACTION,
packageName, uid))
.build();
Notification.Action userDisallowAppNotificationAction =
new Notification.Action.Builder(null,
mResources.getText(R.string.wifi_suggestion_action_disallow_app),
getPrivateBroadcast(NOTIFICATION_USER_DISALLOWED_APP_INTENT_ACTION,
packageName, uid))
.build();
CharSequence appName = getAppName(packageName);
Notification notification = new Notification.Builder(
mContext, SystemNotificationChannels.NETWORK_STATUS)
.setSmallIcon(R.drawable.stat_notify_wifi_in_range)
.setTicker(mResources.getString(R.string.wifi_suggestion_title))
.setContentTitle(mResources.getString(R.string.wifi_suggestion_title))
.setContentText(mResources.getString(R.string.wifi_suggestion_content, appName))
.setDeleteIntent(getPrivateBroadcast(NOTIFICATION_USER_DISMISSED_INTENT_ACTION,
packageName, uid))
.setShowWhen(false)
.setLocalOnly(true)
.setColor(mResources.getColor(R.color.system_notification_accent_color,
mContext.getTheme()))
.addAction(userAllowAppNotificationAction)
.addAction(userDisallowAppNotificationAction)
.build();
// Post the notification.
mNotificationManager.notify(
SystemMessage.NOTE_NETWORK_SUGGESTION_AVAILABLE, notification);
mUserApprovalNotificationActive = true;
mUserApprovalNotificationPackageName = packageName;
}
private boolean sendUserApprovalNotificationIfNotApproved(
@NonNull PerAppInfo perAppInfo,
@NonNull WifiNetworkSuggestion matchingSuggestion) {
if (perAppInfo.hasUserApproved) {
return false; // already approved.
}
Log.i(TAG, "Sending user approval notification for " + perAppInfo.packageName);
sendUserApprovalNotification(perAppInfo.packageName, matchingSuggestion.suggestorUid);
return true;
}
private @Nullable Set<ExtendedWifiNetworkSuggestion>
getNetworkSuggestionsForScanResultMatchInfo(
@NonNull ScanResultMatchInfo scanResultMatchInfo, @Nullable MacAddress bssid) {
Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions = new HashSet<>();
if (bssid != null) {
Set<ExtendedWifiNetworkSuggestion> matchingExtNetworkSuggestionsWithBssid =
mActiveScanResultMatchInfoWithBssid.get(
Pair.create(scanResultMatchInfo, bssid));
if (matchingExtNetworkSuggestionsWithBssid != null) {
extNetworkSuggestions.addAll(matchingExtNetworkSuggestionsWithBssid);
}
}
Set<ExtendedWifiNetworkSuggestion> matchingNetworkSuggestionsWithNoBssid =
mActiveScanResultMatchInfoWithNoBssid.get(scanResultMatchInfo);
if (matchingNetworkSuggestionsWithNoBssid != null) {
extNetworkSuggestions.addAll(matchingNetworkSuggestionsWithNoBssid);
}
if (extNetworkSuggestions.isEmpty()) {
return null;
}
return extNetworkSuggestions;
}
/**
* Returns a set of all network suggestions matching the provided scan detail.
*/
public @Nullable Set<WifiNetworkSuggestion> getNetworkSuggestionsForScanDetail(
@NonNull ScanDetail scanDetail) {
ScanResult scanResult = scanDetail.getScanResult();
if (scanResult == null) {
Log.e(TAG, "No scan result found in scan detail");
return null;
}
Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions = null;
try {
ScanResultMatchInfo scanResultMatchInfo =
ScanResultMatchInfo.fromScanResult(scanResult);
extNetworkSuggestions = getNetworkSuggestionsForScanResultMatchInfo(
scanResultMatchInfo, MacAddress.fromString(scanResult.BSSID));
} catch (IllegalArgumentException e) {
Log.e(TAG, "Failed to lookup network from scan result match info map", e);
}
if (extNetworkSuggestions == null) {
return null;
}
Set<ExtendedWifiNetworkSuggestion> approvedExtNetworkSuggestions =
extNetworkSuggestions
.stream()
.filter(n -> n.perAppInfo.hasUserApproved)
.collect(Collectors.toSet());
// If there is no active notification, check if we need to get approval for any of the apps
// & send a notification for one of them. If there are multiple packages awaiting approval,
// we end up picking the first one. The others will be reconsidered in the next iteration.
if (!mUserApprovalNotificationActive
&& approvedExtNetworkSuggestions.size() != extNetworkSuggestions.size()) {
for (ExtendedWifiNetworkSuggestion extNetworkSuggestion : extNetworkSuggestions) {
if (sendUserApprovalNotificationIfNotApproved(
extNetworkSuggestion.perAppInfo, extNetworkSuggestion.wns)) {
break;
}
}
}
if (approvedExtNetworkSuggestions.isEmpty()) {
return null;
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "getNetworkSuggestionsForScanDetail Found "
+ approvedExtNetworkSuggestions + " for " + scanResult.SSID
+ "[" + scanResult.capabilities + "]");
}
return convertToWnsSet(approvedExtNetworkSuggestions);
}
/**
* Returns a set of all network suggestions matching the provided the WifiConfiguration.
*/
private @Nullable Set<ExtendedWifiNetworkSuggestion> getNetworkSuggestionsForWifiConfiguration(
@NonNull WifiConfiguration wifiConfiguration, @Nullable String bssid) {
Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions = null;
try {
ScanResultMatchInfo scanResultMatchInfo =
ScanResultMatchInfo.fromWifiConfiguration(wifiConfiguration);
extNetworkSuggestions = getNetworkSuggestionsForScanResultMatchInfo(
scanResultMatchInfo, bssid == null ? null : MacAddress.fromString(bssid));
} catch (IllegalArgumentException e) {
Log.e(TAG, "Failed to lookup network from scan result match info map", e);
}
if (extNetworkSuggestions == null) {
return null;
}
Set<ExtendedWifiNetworkSuggestion> approvedExtNetworkSuggestions =
extNetworkSuggestions
.stream()
.filter(n -> n.perAppInfo.hasUserApproved)
.collect(Collectors.toSet());
if (approvedExtNetworkSuggestions.isEmpty()) {
return null;
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "getNetworkSuggestionsFoWifiConfiguration Found "
+ approvedExtNetworkSuggestions + " for " + wifiConfiguration.SSID
+ "[" + wifiConfiguration.allowedKeyManagement + "]");
}
return approvedExtNetworkSuggestions;
}
/**
* Helper method to send the post connection broadcast to specified package.
*/
private void sendPostConnectionBroadcast(
String packageName, WifiNetworkSuggestion networkSuggestion) {
Intent intent = new Intent(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION);
intent.putExtra(WifiManager.EXTRA_NETWORK_SUGGESTION, networkSuggestion);
// Intended to wakeup the receiving app so set the specific package name.
intent.setPackage(packageName);
mContext.sendBroadcastAsUser(
intent, UserHandle.getUserHandleForUid(networkSuggestion.suggestorUid));
}
/**
* Helper method to send the post connection broadcast to specified package.
*/
private void sendPostConnectionBroadcastIfAllowed(
String packageName, WifiNetworkSuggestion matchingSuggestion) {
try {
mWifiPermissionsUtil.enforceCanAccessScanResults(
packageName, matchingSuggestion.suggestorUid);
} catch (SecurityException se) {
return;
}
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Sending post connection broadcast to " + packageName);
}
sendPostConnectionBroadcast(packageName, matchingSuggestion);
}
/**
* Send out the {@link WifiManager#ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION} to all the
* network suggestion credentials that match the current connection network.
*
* @param connectedNetwork {@link WifiConfiguration} representing the network connected to.
* @param connectedBssid BSSID of the network connected to.
*/
private void handleConnectionSuccess(
@NonNull WifiConfiguration connectedNetwork, @NonNull String connectedBssid) {
Set<ExtendedWifiNetworkSuggestion> matchingExtNetworkSuggestions =
getNetworkSuggestionsForWifiConfiguration(connectedNetwork, connectedBssid);
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Network suggestions matching the connection "
+ matchingExtNetworkSuggestions);
}
if (matchingExtNetworkSuggestions == null
|| matchingExtNetworkSuggestions.isEmpty()) return;
mWifiMetrics.incrementNetworkSuggestionApiNumConnectSuccess();
// Store the set of matching network suggestions.
mActiveNetworkSuggestionsMatchingConnection = new HashSet<>(matchingExtNetworkSuggestions);
// Find subset of network suggestions which have set |isAppInteractionRequired|.
Set<ExtendedWifiNetworkSuggestion> matchingExtNetworkSuggestionsWithReqAppInteraction =
matchingExtNetworkSuggestions.stream()
.filter(x -> x.wns.isAppInteractionRequired)
.collect(Collectors.toSet());
if (matchingExtNetworkSuggestionsWithReqAppInteraction.size() == 0) return;
// Iterate over the matching network suggestions list:
// a) Ensure that these apps have the necessary location permissions.
// b) Send directed broadcast to the app with their corresponding network suggestion.
for (ExtendedWifiNetworkSuggestion matchingExtNetworkSuggestion
: matchingExtNetworkSuggestionsWithReqAppInteraction) {
sendPostConnectionBroadcastIfAllowed(
matchingExtNetworkSuggestion.perAppInfo.packageName,
matchingExtNetworkSuggestion.wns);
}
}
/**
* Handle connection failure.
*
* @param network {@link WifiConfiguration} representing the network that connection failed to.
* @param bssid BSSID of the network connection failed to if known, else null.
*/
private void handleConnectionFailure(@NonNull WifiConfiguration network,
@Nullable String bssid) {
Set<ExtendedWifiNetworkSuggestion> matchingExtNetworkSuggestions =
getNetworkSuggestionsForWifiConfiguration(network, bssid);
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Network suggestions matching the connection failure "
+ matchingExtNetworkSuggestions);
}
if (matchingExtNetworkSuggestions == null
|| matchingExtNetworkSuggestions.isEmpty()) return;
mWifiMetrics.incrementNetworkSuggestionApiNumConnectFailure();
// TODO (b/115504887, b/112196799): Blacklist the corresponding network suggestion if
// the connection failed.
}
private void resetConnectionState() {
mActiveNetworkSuggestionsMatchingConnection = null;
}
/**
* Invoked by {@link ClientModeImpl} on end of connection attempt to a network.
*
* @param failureCode Failure codes representing {@link WifiMetrics.ConnectionEvent} codes.
* @param network WifiConfiguration corresponding to the current network.
* @param bssid BSSID of the current network.
*/
public void handleConnectionAttemptEnded(
int failureCode, @NonNull WifiConfiguration network, @Nullable String bssid) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "handleConnectionAttemptEnded " + failureCode + ", " + network);
}
resetConnectionState();
if (failureCode == WifiMetrics.ConnectionEvent.FAILURE_NONE) {
handleConnectionSuccess(network, bssid);
} else {
handleConnectionFailure(network, bssid);
}
}
/**
* Invoked by {@link ClientModeImpl} on disconnect from network.
*/
public void handleDisconnect(@NonNull WifiConfiguration network, @NonNull String bssid) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "handleDisconnect " + network);
}
resetConnectionState();
}
/**
* Dump of {@link WifiNetworkSuggestionsManager}.
*/
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("Dump of WifiNetworkSuggestionsManager");
pw.println("WifiNetworkSuggestionsManager - Networks Begin ----");
for (Map.Entry<String, PerAppInfo> networkSuggestionsEntry
: mActiveNetworkSuggestionsPerApp.entrySet()) {
pw.println("Package Name: " + networkSuggestionsEntry.getKey());
PerAppInfo appInfo = networkSuggestionsEntry.getValue();
pw.println("Has user approved: " + appInfo.hasUserApproved);
for (ExtendedWifiNetworkSuggestion extNetworkSuggestion
: appInfo.extNetworkSuggestions) {
pw.println("Network: " + extNetworkSuggestion);
}
}
pw.println("WifiNetworkSuggestionsManager - Networks End ----");
pw.println("WifiNetworkSuggestionsManager - Network Suggestions matching connection: "
+ mActiveNetworkSuggestionsMatchingConnection);
}
}