blob: 7243193cdf5a847e5d75ac7f9f76739c5d67be64 [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 com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_CONNECT_TO_NETWORK;
import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_PICK_WIFI_NETWORK;
import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE;
import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_USER_DISMISSED_NOTIFICATION;
import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.AVAILABLE_NETWORK_NOTIFIER_TAG;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.net.wifi.IActionListener;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.os.Binder;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wifi.proto.nano.WifiMetricsProto
.ConnectToNetworkNotificationAndActionCount;
import com.android.server.wifi.util.ScanResultUtil;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Set;
/**
* Base class for all network notifiers (e.g. OpenNetworkNotifier).
*
* NOTE: These API's are not thread safe and should only be used from WifiCoreThread.
*/
public class AvailableNetworkNotifier {
/** Time in milliseconds to display the Connecting notification. */
private static final int TIME_TO_SHOW_CONNECTING_MILLIS = 10000;
/** Time in milliseconds to display the Connected notification. */
private static final int TIME_TO_SHOW_CONNECTED_MILLIS = 5000;
/** Time in milliseconds to display the Failed To Connect notification. */
private static final int TIME_TO_SHOW_FAILED_MILLIS = 5000;
/** The state of the notification */
@IntDef({
STATE_NO_NOTIFICATION,
STATE_SHOWING_RECOMMENDATION_NOTIFICATION,
STATE_CONNECTING_IN_NOTIFICATION,
STATE_CONNECTED_NOTIFICATION,
STATE_CONNECT_FAILED_NOTIFICATION
})
@Retention(RetentionPolicy.SOURCE)
private @interface State {}
/** No recommendation is made and no notifications are shown. */
private static final int STATE_NO_NOTIFICATION = 0;
/** The initial notification recommending a network to connect to is shown. */
private static final int STATE_SHOWING_RECOMMENDATION_NOTIFICATION = 1;
/** The notification of status of connecting to the recommended network is shown. */
private static final int STATE_CONNECTING_IN_NOTIFICATION = 2;
/** The notification that the connection to the recommended network was successful is shown. */
private static final int STATE_CONNECTED_NOTIFICATION = 3;
/** The notification to show that connection to the recommended network failed is shown. */
private static final int STATE_CONNECT_FAILED_NOTIFICATION = 4;
/** Current state of the notification. */
@State private int mState = STATE_NO_NOTIFICATION;
/**
* The {@link Clock#getWallClockMillis()} must be at least this value for us
* to show the notification again.
*/
private long mNotificationRepeatTime;
/**
* When a notification is shown, we wait this amount before possibly showing it again.
*/
private final long mNotificationRepeatDelay;
/** Default repeat delay in seconds. */
@VisibleForTesting
static final int DEFAULT_REPEAT_DELAY_SEC = 900;
/** Whether the user has set the setting to show the 'available networks' notification. */
private boolean mSettingEnabled;
/** Whether the screen is on or not. */
private boolean mScreenOn;
/** List of SSIDs blacklisted from recommendation. */
private final Set<String> mBlacklistedSsids = new ArraySet<>();
private final Context mContext;
private final Handler mHandler;
private final FrameworkFacade mFrameworkFacade;
private final WifiMetrics mWifiMetrics;
private final Clock mClock;
private final WifiConfigManager mConfigManager;
private final ClientModeImpl mClientModeImpl;
private final ConnectToNetworkNotificationBuilder mNotificationBuilder;
private ScanResult mRecommendedNetwork;
/** Tag used for logs and metrics */
private final String mTag;
/** Identifier of the {@link SsidSetStoreData}. */
private final String mStoreDataIdentifier;
/** Identifier for the settings toggle, used for registering ContentObserver */
private final String mToggleSettingsName;
/** System wide identifier for notification in Notification Manager */
private final int mSystemMessageNotificationId;
/**
* The nominator id for this class, from
* {@link com.android.server.wifi.proto.nano.WifiMetricsProto.ConnectionEvent.
* ConnectionNominator}
*/
private final int mNominatorId;
public AvailableNetworkNotifier(
String tag,
String storeDataIdentifier,
String toggleSettingsName,
int notificationIdentifier,
int nominatorId,
Context context,
Looper looper,
FrameworkFacade framework,
Clock clock,
WifiMetrics wifiMetrics,
WifiConfigManager wifiConfigManager,
WifiConfigStore wifiConfigStore,
ClientModeImpl clientModeImpl,
ConnectToNetworkNotificationBuilder connectToNetworkNotificationBuilder) {
mTag = tag;
mStoreDataIdentifier = storeDataIdentifier;
mToggleSettingsName = toggleSettingsName;
mSystemMessageNotificationId = notificationIdentifier;
mNominatorId = nominatorId;
mContext = context;
mHandler = new Handler(looper);
mFrameworkFacade = framework;
mWifiMetrics = wifiMetrics;
mClock = clock;
mConfigManager = wifiConfigManager;
mClientModeImpl = clientModeImpl;
mNotificationBuilder = connectToNetworkNotificationBuilder;
mScreenOn = false;
wifiConfigStore.registerStoreData(new SsidSetStoreData(mStoreDataIdentifier,
new AvailableNetworkNotifierStoreData()));
// Setting is in seconds
mNotificationRepeatDelay = mFrameworkFacade.getIntegerSetting(context,
Settings.Global.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY,
DEFAULT_REPEAT_DELAY_SEC) * 1000L;
NotificationEnabledSettingObserver settingObserver = new NotificationEnabledSettingObserver(
mHandler);
settingObserver.register();
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_USER_DISMISSED_NOTIFICATION);
filter.addAction(ACTION_CONNECT_TO_NETWORK);
filter.addAction(ACTION_PICK_WIFI_NETWORK);
filter.addAction(ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE);
mContext.registerReceiver(
mBroadcastReceiver, filter, null /* broadcastPermission */, mHandler);
}
private final BroadcastReceiver mBroadcastReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!mTag.equals(intent.getStringExtra(AVAILABLE_NETWORK_NOTIFIER_TAG))) {
return;
}
switch (intent.getAction()) {
case ACTION_USER_DISMISSED_NOTIFICATION:
handleUserDismissedAction();
break;
case ACTION_CONNECT_TO_NETWORK:
handleConnectToNetworkAction();
break;
case ACTION_PICK_WIFI_NETWORK:
handleSeeAllNetworksAction();
break;
case ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE:
handlePickWifiNetworkAfterConnectFailure();
break;
default:
Log.e(mTag, "Unknown action " + intent.getAction());
}
}
};
private final class ConnectActionListener extends IActionListener.Stub {
@Override
public void onSuccess() {
// Success here means that an attempt to connect to the network has been initiated.
// Successful connection updates are received via the
// WifiConnectivityManager#handleConnectionStateChanged() callback.
}
@Override
public void onFailure(int reason) {
handleConnectionAttemptFailedToSend();
}
}
/**
* Clears the pending notification. This is called by {@link WifiConnectivityManager} on stop.
*
* @param resetRepeatTime resets the time delay for repeated notification if true.
*/
public void clearPendingNotification(boolean resetRepeatTime) {
if (resetRepeatTime) {
mNotificationRepeatTime = 0;
}
if (mState != STATE_NO_NOTIFICATION) {
getNotificationManager().cancel(mSystemMessageNotificationId);
if (mRecommendedNetwork != null) {
Log.d(mTag, "Notification with state="
+ mState
+ " was cleared for recommended network: "
+ "\"" + mRecommendedNetwork.SSID + "\"");
}
mState = STATE_NO_NOTIFICATION;
mRecommendedNetwork = null;
}
}
private boolean isControllerEnabled() {
return mSettingEnabled && !mContext.getSystemService(UserManager.class)
.hasUserRestrictionForUser(UserManager.DISALLOW_CONFIG_WIFI,
UserHandle.CURRENT);
}
/**
* If there are available networks, attempt to post a network notification.
*
* @param availableNetworks Available networks to choose from and possibly show notification
*/
public void handleScanResults(@NonNull List<ScanDetail> availableNetworks) {
if (!isControllerEnabled()) {
clearPendingNotification(true /* resetRepeatTime */);
return;
}
if (availableNetworks.isEmpty() && mState == STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
clearPendingNotification(false /* resetRepeatTime */);
return;
}
// Not enough time has passed to show a recommendation notification again
if (mState == STATE_NO_NOTIFICATION
&& mClock.getWallClockMillis() < mNotificationRepeatTime) {
return;
}
// Do nothing when the screen is off and no notification is showing.
if (mState == STATE_NO_NOTIFICATION && !mScreenOn) {
return;
}
// Only show a new or update an existing recommendation notification.
if (mState == STATE_NO_NOTIFICATION
|| mState == STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
ScanResult recommendation =
recommendNetwork(availableNetworks);
if (recommendation != null) {
postInitialNotification(recommendation);
} else {
clearPendingNotification(false /* resetRepeatTime */);
}
}
}
/**
* Recommends a network to connect to from a list of available networks, while ignoring the
* SSIDs in the blacklist.
*
* @param networks List of networks to select from
*/
public ScanResult recommendNetwork(@NonNull List<ScanDetail> networks) {
ScanResult result = null;
int highestRssi = Integer.MIN_VALUE;
for (ScanDetail scanDetail : networks) {
ScanResult scanResult = scanDetail.getScanResult();
if (scanResult.level > highestRssi) {
result = scanResult;
highestRssi = scanResult.level;
}
}
if (result != null && mBlacklistedSsids.contains(result.SSID)) {
result = null;
}
return result;
}
/** Handles screen state changes. */
public void handleScreenStateChanged(boolean screenOn) {
mScreenOn = screenOn;
}
/**
* Called by {@link WifiConnectivityManager} when Wi-Fi is connected. If the notification
* was in the connecting state, update the notification to show that it has connected to the
* recommended network.
*
* @param ssid The connected network's ssid
*/
public void handleWifiConnected(String ssid) {
removeNetworkFromBlacklist(ssid);
if (mState != STATE_CONNECTING_IN_NOTIFICATION) {
clearPendingNotification(true /* resetRepeatTime */);
return;
}
postNotification(mNotificationBuilder.createNetworkConnectedNotification(mTag,
mRecommendedNetwork));
Log.d(mTag, "User connected to recommended network: "
+ "\"" + mRecommendedNetwork.SSID + "\"");
mWifiMetrics.incrementConnectToNetworkNotification(mTag,
ConnectToNetworkNotificationAndActionCount.NOTIFICATION_CONNECTED_TO_NETWORK);
mState = STATE_CONNECTED_NOTIFICATION;
mHandler.postDelayed(
() -> {
if (mState == STATE_CONNECTED_NOTIFICATION) {
clearPendingNotification(true /* resetRepeatTime */);
}
},
TIME_TO_SHOW_CONNECTED_MILLIS);
}
/**
* Handles when a Wi-Fi connection attempt failed.
*/
public void handleConnectionFailure() {
if (mState != STATE_CONNECTING_IN_NOTIFICATION) {
return;
}
postNotification(mNotificationBuilder.createNetworkFailedNotification(mTag));
Log.d(mTag, "User failed to connect to recommended network: "
+ "\"" + mRecommendedNetwork.SSID + "\"");
mWifiMetrics.incrementConnectToNetworkNotification(mTag,
ConnectToNetworkNotificationAndActionCount.NOTIFICATION_FAILED_TO_CONNECT);
mState = STATE_CONNECT_FAILED_NOTIFICATION;
mHandler.postDelayed(
() -> {
if (mState == STATE_CONNECT_FAILED_NOTIFICATION) {
clearPendingNotification(false /* resetRepeatTime */);
}
},
TIME_TO_SHOW_FAILED_MILLIS);
}
private NotificationManager getNotificationManager() {
return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
private void postInitialNotification(ScanResult recommendedNetwork) {
if (mRecommendedNetwork != null
&& TextUtils.equals(mRecommendedNetwork.SSID, recommendedNetwork.SSID)) {
return;
}
postNotification(mNotificationBuilder.createConnectToAvailableNetworkNotification(mTag,
recommendedNetwork));
if (mState == STATE_NO_NOTIFICATION) {
mWifiMetrics.incrementConnectToNetworkNotification(mTag,
ConnectToNetworkNotificationAndActionCount.NOTIFICATION_RECOMMEND_NETWORK);
} else {
mWifiMetrics.incrementNumNetworkRecommendationUpdates(mTag);
}
mState = STATE_SHOWING_RECOMMENDATION_NOTIFICATION;
mRecommendedNetwork = recommendedNetwork;
mNotificationRepeatTime = mClock.getWallClockMillis() + mNotificationRepeatDelay;
}
private void postNotification(Notification notification) {
getNotificationManager().notify(mSystemMessageNotificationId, notification);
}
private void handleConnectToNetworkAction() {
mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
ConnectToNetworkNotificationAndActionCount.ACTION_CONNECT_TO_NETWORK);
if (mState != STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
return;
}
postNotification(mNotificationBuilder.createNetworkConnectingNotification(mTag,
mRecommendedNetwork));
mWifiMetrics.incrementConnectToNetworkNotification(mTag,
ConnectToNetworkNotificationAndActionCount.NOTIFICATION_CONNECTING_TO_NETWORK);
Log.d(mTag,
"User initiated connection to recommended network: "
+ "\"" + mRecommendedNetwork.SSID + "\"");
WifiConfiguration network = createRecommendedNetworkConfig(mRecommendedNetwork);
NetworkUpdateResult result = mConfigManager.addOrUpdateNetwork(network, Process.WIFI_UID);
if (result.isSuccess()) {
mWifiMetrics.setNominatorForNetwork(result.netId, mNominatorId);
ConnectActionListener connectActionListener = new ConnectActionListener();
mClientModeImpl.connect(null, result.netId, new Binder(), connectActionListener,
connectActionListener.hashCode(), Process.SYSTEM_UID);
addNetworkToBlacklist(mRecommendedNetwork.SSID);
}
mState = STATE_CONNECTING_IN_NOTIFICATION;
mHandler.postDelayed(
() -> {
if (mState == STATE_CONNECTING_IN_NOTIFICATION) {
handleConnectionFailure();
}
},
TIME_TO_SHOW_CONNECTING_MILLIS);
}
private void addNetworkToBlacklist(String ssid) {
mBlacklistedSsids.add(ssid);
mWifiMetrics.setNetworkRecommenderBlacklistSize(mTag, mBlacklistedSsids.size());
mConfigManager.saveToStore(false /* forceWrite */);
Log.d(mTag, "Network is added to the network notification blacklist: "
+ "\"" + ssid + "\"");
}
private void removeNetworkFromBlacklist(String ssid) {
if (ssid == null) {
return;
}
if (!mBlacklistedSsids.remove(ssid)) {
return;
}
mWifiMetrics.setNetworkRecommenderBlacklistSize(mTag, mBlacklistedSsids.size());
mConfigManager.saveToStore(false /* forceWrite */);
Log.d(mTag, "Network is removed from the network notification blacklist: "
+ "\"" + ssid + "\"");
}
WifiConfiguration createRecommendedNetworkConfig(ScanResult recommendedNetwork) {
return ScanResultUtil.createNetworkFromScanResult(recommendedNetwork);
}
private void handleSeeAllNetworksAction() {
mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
ConnectToNetworkNotificationAndActionCount.ACTION_PICK_WIFI_NETWORK);
startWifiSettings();
}
private void startWifiSettings() {
// Close notification drawer before opening the picker.
mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
mContext.startActivity(
new Intent(Settings.ACTION_WIFI_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
clearPendingNotification(false /* resetRepeatTime */);
}
private void handleConnectionAttemptFailedToSend() {
handleConnectionFailure();
mWifiMetrics.incrementNumNetworkConnectMessageFailedToSend(mTag);
}
private void handlePickWifiNetworkAfterConnectFailure() {
mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
ConnectToNetworkNotificationAndActionCount
.ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE);
startWifiSettings();
}
private void handleUserDismissedAction() {
Log.d(mTag, "User dismissed notification with state=" + mState);
mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
ConnectToNetworkNotificationAndActionCount.ACTION_USER_DISMISSED_NOTIFICATION);
if (mState == STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
// blacklist dismissed network
addNetworkToBlacklist(mRecommendedNetwork.SSID);
}
resetStateAndDelayNotification();
}
private void resetStateAndDelayNotification() {
mState = STATE_NO_NOTIFICATION;
mNotificationRepeatTime = System.currentTimeMillis() + mNotificationRepeatDelay;
mRecommendedNetwork = null;
}
/** Dump this network notifier's state. */
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println(mTag + ": ");
pw.println("mSettingEnabled " + mSettingEnabled);
pw.println("currentTime: " + mClock.getWallClockMillis());
pw.println("mNotificationRepeatTime: " + mNotificationRepeatTime);
pw.println("mState: " + mState);
pw.println("mBlacklistedSsids: " + mBlacklistedSsids.toString());
}
private class AvailableNetworkNotifierStoreData implements SsidSetStoreData.DataSource {
@Override
public Set<String> getSsids() {
return new ArraySet<>(mBlacklistedSsids);
}
@Override
public void setSsids(Set<String> ssidList) {
mBlacklistedSsids.addAll(ssidList);
mWifiMetrics.setNetworkRecommenderBlacklistSize(mTag, mBlacklistedSsids.size());
}
}
private class NotificationEnabledSettingObserver extends ContentObserver {
NotificationEnabledSettingObserver(Handler handler) {
super(handler);
}
public void register() {
mFrameworkFacade.registerContentObserver(mContext,
Settings.Global.getUriFor(mToggleSettingsName), true, this);
mSettingEnabled = getValue();
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
mSettingEnabled = getValue();
clearPendingNotification(true /* resetRepeatTime */);
}
private boolean getValue() {
boolean enabled =
mFrameworkFacade.getIntegerSetting(mContext, mToggleSettingsName, 1) == 1;
mWifiMetrics.setIsWifiNetworksAvailableNotificationEnabled(mTag, enabled);
Log.d(mTag, "Settings toggle enabled=" + enabled);
return enabled;
}
}
}