/*
|
* 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.ScanResult;
|
import android.net.wifi.WifiConfiguration;
|
import android.net.wifi.WifiManager;
|
import android.os.Handler;
|
import android.os.Looper;
|
import android.os.Message;
|
import android.os.Messenger;
|
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.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, CarrierNetworkNotifier).
|
*
|
* 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 Messenger mSrcMessenger;
|
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.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;
|
mSrcMessenger = new Messenger(new Handler(looper, mConnectionStateCallback));
|
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.getExtra(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 Handler.Callback mConnectionStateCallback = (Message msg) -> {
|
switch (msg.what) {
|
// Success here means that an attempt to connect to the network has been initiated.
|
// Successful connection updates are received via the
|
// WifiConnectivityManager#handleConnectionStateChanged() callback.
|
case WifiManager.CONNECT_NETWORK_SUCCEEDED:
|
break;
|
case WifiManager.CONNECT_NETWORK_FAILED:
|
handleConnectionAttemptFailedToSend();
|
break;
|
default:
|
Log.e("AvailableNetworkNotifier", "Unknown message " + msg.what);
|
}
|
return true;
|
};
|
|
/**
|
* 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 && !UserManager.get(mContext)
|
.hasUserRestriction(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);
|
|
Message msg = Message.obtain();
|
msg.what = WifiManager.CONNECT_NETWORK;
|
msg.arg1 = result.netId;
|
msg.obj = null;
|
msg.replyTo = mSrcMessenger;
|
mClientModeImpl.sendMessage(msg);
|
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;
|
}
|
}
|
}
|