1/*
2 * Copyright (C) 2018 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
17package com.android.server.wifi;
18
19import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_CONNECT_TO_NETWORK;
20import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_PICK_WIFI_NETWORK;
21import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE;
22import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_USER_DISMISSED_NOTIFICATION;
23import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.AVAILABLE_NETWORK_NOTIFIER_TAG;
24
25import android.annotation.IntDef;
26import android.annotation.NonNull;
27import android.app.Notification;
28import android.app.NotificationManager;
29import android.content.BroadcastReceiver;
30import android.content.Context;
31import android.content.Intent;
32import android.content.IntentFilter;
33import android.database.ContentObserver;
34import android.net.wifi.ScanResult;
35import android.net.wifi.WifiConfiguration;
36import android.net.wifi.WifiManager;
37import android.os.Handler;
38import android.os.Looper;
39import android.os.Message;
40import android.os.Messenger;
41import android.os.UserHandle;
42import android.os.UserManager;
43import android.provider.Settings;
44import android.text.TextUtils;
45import android.util.ArraySet;
46import android.util.Log;
47
48import com.android.internal.annotations.VisibleForTesting;
49import com.android.server.wifi.nano.WifiMetricsProto.ConnectToNetworkNotificationAndActionCount;
50import com.android.server.wifi.util.ScanResultUtil;
51
52import java.io.FileDescriptor;
53import java.io.PrintWriter;
54import java.lang.annotation.Retention;
55import java.lang.annotation.RetentionPolicy;
56import java.util.List;
57import java.util.Set;
58
59/**
60 * Base class for all network notifiers (e.g. OpenNetworkNotifier, CarrierNetworkNotifier).
61 *
62 * NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread.
63 */
64public class AvailableNetworkNotifier {
65
66    /** Time in milliseconds to display the Connecting notification. */
67    private static final int TIME_TO_SHOW_CONNECTING_MILLIS = 10000;
68
69    /** Time in milliseconds to display the Connected notification. */
70    private static final int TIME_TO_SHOW_CONNECTED_MILLIS = 5000;
71
72    /** Time in milliseconds to display the Failed To Connect notification. */
73    private static final int TIME_TO_SHOW_FAILED_MILLIS = 5000;
74
75    /** The state of the notification */
76    @IntDef({
77            STATE_NO_NOTIFICATION,
78            STATE_SHOWING_RECOMMENDATION_NOTIFICATION,
79            STATE_CONNECTING_IN_NOTIFICATION,
80            STATE_CONNECTED_NOTIFICATION,
81            STATE_CONNECT_FAILED_NOTIFICATION
82    })
83    @Retention(RetentionPolicy.SOURCE)
84    private @interface State {}
85
86    /** No recommendation is made and no notifications are shown. */
87    private static final int STATE_NO_NOTIFICATION = 0;
88    /** The initial notification recommending a network to connect to is shown. */
89    private static final int STATE_SHOWING_RECOMMENDATION_NOTIFICATION = 1;
90    /** The notification of status of connecting to the recommended network is shown. */
91    private static final int STATE_CONNECTING_IN_NOTIFICATION = 2;
92    /** The notification that the connection to the recommended network was successful is shown. */
93    private static final int STATE_CONNECTED_NOTIFICATION = 3;
94    /** The notification to show that connection to the recommended network failed is shown. */
95    private static final int STATE_CONNECT_FAILED_NOTIFICATION = 4;
96
97    /** Current state of the notification. */
98    @State private int mState = STATE_NO_NOTIFICATION;
99
100    /**
101     * The {@link Clock#getWallClockMillis()} must be at least this value for us
102     * to show the notification again.
103     */
104    private long mNotificationRepeatTime;
105    /**
106     * When a notification is shown, we wait this amount before possibly showing it again.
107     */
108    private final long mNotificationRepeatDelay;
109    /** Default repeat delay in seconds. */
110    @VisibleForTesting
111    static final int DEFAULT_REPEAT_DELAY_SEC = 900;
112
113    /** Whether the user has set the setting to show the 'available networks' notification. */
114    private boolean mSettingEnabled;
115    /** Whether the screen is on or not. */
116    private boolean mScreenOn;
117
118    /** List of SSIDs blacklisted from recommendation. */
119    private final Set<String> mBlacklistedSsids;
120
121    private final Context mContext;
122    private final Handler mHandler;
123    private final FrameworkFacade mFrameworkFacade;
124    private final WifiMetrics mWifiMetrics;
125    private final Clock mClock;
126    private final WifiConfigManager mConfigManager;
127    private final WifiStateMachine mWifiStateMachine;
128    private final Messenger mSrcMessenger;
129    private final ConnectToNetworkNotificationBuilder mNotificationBuilder;
130
131    private ScanResult mRecommendedNetwork;
132
133    /** Tag used for logs and metrics */
134    private final String mTag;
135    /** Identifier of the {@link SsidSetStoreData}. */
136    private final String mStoreDataIdentifier;
137    /** Identifier for the settings toggle, used for registering ContentObserver */
138    private final String mToggleSettingsName;
139
140    /** System wide identifier for notification in Notification Manager */
141    private final int mSystemMessageNotificationId;
142
143    public AvailableNetworkNotifier(
144            String tag,
145            String storeDataIdentifier,
146            String toggleSettingsName,
147            int notificationIdentifier,
148            Context context,
149            Looper looper,
150            FrameworkFacade framework,
151            Clock clock,
152            WifiMetrics wifiMetrics,
153            WifiConfigManager wifiConfigManager,
154            WifiConfigStore wifiConfigStore,
155            WifiStateMachine wifiStateMachine,
156            ConnectToNetworkNotificationBuilder connectToNetworkNotificationBuilder) {
157        mTag = tag;
158        mStoreDataIdentifier = storeDataIdentifier;
159        mToggleSettingsName = toggleSettingsName;
160        mSystemMessageNotificationId = notificationIdentifier;
161        mContext = context;
162        mHandler = new Handler(looper);
163        mFrameworkFacade = framework;
164        mWifiMetrics = wifiMetrics;
165        mClock = clock;
166        mConfigManager = wifiConfigManager;
167        mWifiStateMachine = wifiStateMachine;
168        mNotificationBuilder = connectToNetworkNotificationBuilder;
169        mScreenOn = false;
170        mSrcMessenger = new Messenger(new Handler(looper, mConnectionStateCallback));
171
172        mBlacklistedSsids = new ArraySet<>();
173        wifiConfigStore.registerStoreData(new SsidSetStoreData(mStoreDataIdentifier,
174                new AvailableNetworkNotifierStoreData()));
175
176        // Setting is in seconds
177        mNotificationRepeatDelay = mFrameworkFacade.getIntegerSetting(context,
178                Settings.Global.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY,
179                DEFAULT_REPEAT_DELAY_SEC) * 1000L;
180        NotificationEnabledSettingObserver settingObserver = new NotificationEnabledSettingObserver(
181                mHandler);
182        settingObserver.register();
183
184        IntentFilter filter = new IntentFilter();
185        filter.addAction(ACTION_USER_DISMISSED_NOTIFICATION);
186        filter.addAction(ACTION_CONNECT_TO_NETWORK);
187        filter.addAction(ACTION_PICK_WIFI_NETWORK);
188        filter.addAction(ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE);
189        mContext.registerReceiver(
190                mBroadcastReceiver, filter, null /* broadcastPermission */, mHandler);
191    }
192
193    private final BroadcastReceiver mBroadcastReceiver =
194            new BroadcastReceiver() {
195                @Override
196                public void onReceive(Context context, Intent intent) {
197                    if (!mTag.equals(intent.getExtra(AVAILABLE_NETWORK_NOTIFIER_TAG))) {
198                        return;
199                    }
200                    switch (intent.getAction()) {
201                        case ACTION_USER_DISMISSED_NOTIFICATION:
202                            handleUserDismissedAction();
203                            break;
204                        case ACTION_CONNECT_TO_NETWORK:
205                            handleConnectToNetworkAction();
206                            break;
207                        case ACTION_PICK_WIFI_NETWORK:
208                            handleSeeAllNetworksAction();
209                            break;
210                        case ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE:
211                            handlePickWifiNetworkAfterConnectFailure();
212                            break;
213                        default:
214                            Log.e(mTag, "Unknown action " + intent.getAction());
215                    }
216                }
217            };
218
219    private final Handler.Callback mConnectionStateCallback = (Message msg) -> {
220        switch (msg.what) {
221            // Success here means that an attempt to connect to the network has been initiated.
222            // Successful connection updates are received via the
223            // WifiConnectivityManager#handleConnectionStateChanged() callback.
224            case WifiManager.CONNECT_NETWORK_SUCCEEDED:
225                break;
226            case WifiManager.CONNECT_NETWORK_FAILED:
227                handleConnectionAttemptFailedToSend();
228                break;
229            default:
230                Log.e("AvailableNetworkNotifier", "Unknown message " + msg.what);
231        }
232        return true;
233    };
234
235    /**
236     * Clears the pending notification. This is called by {@link WifiConnectivityManager} on stop.
237     *
238     * @param resetRepeatTime resets the time delay for repeated notification if true.
239     */
240    public void clearPendingNotification(boolean resetRepeatTime) {
241        if (resetRepeatTime) {
242            mNotificationRepeatTime = 0;
243        }
244
245        if (mState != STATE_NO_NOTIFICATION) {
246            getNotificationManager().cancel(mSystemMessageNotificationId);
247
248            if (mRecommendedNetwork != null) {
249                Log.d(mTag, "Notification with state="
250                        + mState
251                        + " was cleared for recommended network: "
252                        + mRecommendedNetwork.SSID);
253            }
254            mState = STATE_NO_NOTIFICATION;
255            mRecommendedNetwork = null;
256        }
257    }
258
259    private boolean isControllerEnabled() {
260        return mSettingEnabled && !UserManager.get(mContext)
261                .hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI, UserHandle.CURRENT);
262    }
263
264    /**
265     * If there are available networks, attempt to post a network notification.
266     *
267     * @param availableNetworks Available networks to choose from and possibly show notification
268     */
269    public void handleScanResults(@NonNull List<ScanDetail> availableNetworks) {
270        if (!isControllerEnabled()) {
271            clearPendingNotification(true /* resetRepeatTime */);
272            return;
273        }
274        if (availableNetworks.isEmpty() && mState == STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
275            clearPendingNotification(false /* resetRepeatTime */);
276            return;
277        }
278
279        // Not enough time has passed to show a recommendation notification again
280        if (mState == STATE_NO_NOTIFICATION
281                && mClock.getWallClockMillis() < mNotificationRepeatTime) {
282            return;
283        }
284
285        // Do nothing when the screen is off and no notification is showing.
286        if (mState == STATE_NO_NOTIFICATION && !mScreenOn) {
287            return;
288        }
289
290        // Only show a new or update an existing recommendation notification.
291        if (mState == STATE_NO_NOTIFICATION
292                || mState == STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
293            ScanResult recommendation =
294                    recommendNetwork(availableNetworks, new ArraySet<>(mBlacklistedSsids));
295
296            if (recommendation != null) {
297                postInitialNotification(recommendation);
298            } else {
299                clearPendingNotification(false /* resetRepeatTime */);
300            }
301        }
302    }
303
304    /**
305     * Recommends a network to connect to from a list of available networks, while ignoring the
306     * SSIDs in the blacklist.
307     */
308    public ScanResult recommendNetwork(@NonNull List<ScanDetail> networks,
309            @NonNull Set<String> blacklistedSsids) {
310        ScanResult result = null;
311        int highestRssi = Integer.MIN_VALUE;
312        for (ScanDetail scanDetail : networks) {
313            ScanResult scanResult = scanDetail.getScanResult();
314
315            if (scanResult.level > highestRssi) {
316                result = scanResult;
317                highestRssi = scanResult.level;
318            }
319        }
320
321        if (result != null && blacklistedSsids.contains(result.SSID)) {
322            result = null;
323        }
324        return result;
325    }
326
327    /** Handles screen state changes. */
328    public void handleScreenStateChanged(boolean screenOn) {
329        mScreenOn = screenOn;
330    }
331
332    /**
333     * Called by {@link WifiConnectivityManager} when Wi-Fi is connected. If the notification
334     * was in the connecting state, update the notification to show that it has connected to the
335     * recommended network.
336     */
337    public void handleWifiConnected() {
338        if (mState != STATE_CONNECTING_IN_NOTIFICATION) {
339            clearPendingNotification(true /* resetRepeatTime */);
340            return;
341        }
342
343        postNotification(mNotificationBuilder.createNetworkConnectedNotification(mTag,
344                mRecommendedNetwork));
345
346        Log.d(mTag, "User connected to recommended network: " + mRecommendedNetwork.SSID);
347        mWifiMetrics.incrementConnectToNetworkNotification(mTag,
348                ConnectToNetworkNotificationAndActionCount.NOTIFICATION_CONNECTED_TO_NETWORK);
349        mState = STATE_CONNECTED_NOTIFICATION;
350        mHandler.postDelayed(
351                () -> {
352                    if (mState == STATE_CONNECTED_NOTIFICATION) {
353                        clearPendingNotification(true /* resetRepeatTime */);
354                    }
355                },
356                TIME_TO_SHOW_CONNECTED_MILLIS);
357    }
358
359    /**
360     * Handles when a Wi-Fi connection attempt failed.
361     */
362    public void handleConnectionFailure() {
363        if (mState != STATE_CONNECTING_IN_NOTIFICATION) {
364            return;
365        }
366        postNotification(mNotificationBuilder.createNetworkFailedNotification(mTag));
367
368        Log.d(mTag, "User failed to connect to recommended network: " + mRecommendedNetwork.SSID);
369        mWifiMetrics.incrementConnectToNetworkNotification(mTag,
370                ConnectToNetworkNotificationAndActionCount.NOTIFICATION_FAILED_TO_CONNECT);
371        mState = STATE_CONNECT_FAILED_NOTIFICATION;
372        mHandler.postDelayed(
373                () -> {
374                    if (mState == STATE_CONNECT_FAILED_NOTIFICATION) {
375                        clearPendingNotification(false /* resetRepeatTime */);
376                    }
377                },
378                TIME_TO_SHOW_FAILED_MILLIS);
379    }
380
381    private NotificationManager getNotificationManager() {
382        return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
383    }
384
385    private void postInitialNotification(ScanResult recommendedNetwork) {
386        if (mRecommendedNetwork != null
387                && TextUtils.equals(mRecommendedNetwork.SSID, recommendedNetwork.SSID)) {
388            return;
389        }
390
391        postNotification(mNotificationBuilder.createConnectToAvailableNetworkNotification(mTag,
392                recommendedNetwork));
393
394        if (mState == STATE_NO_NOTIFICATION) {
395            mWifiMetrics.incrementConnectToNetworkNotification(mTag,
396                    ConnectToNetworkNotificationAndActionCount.NOTIFICATION_RECOMMEND_NETWORK);
397        } else {
398            mWifiMetrics.incrementNumNetworkRecommendationUpdates(mTag);
399        }
400        mState = STATE_SHOWING_RECOMMENDATION_NOTIFICATION;
401        mRecommendedNetwork = recommendedNetwork;
402        mNotificationRepeatTime = mClock.getWallClockMillis() + mNotificationRepeatDelay;
403    }
404
405    private void postNotification(Notification notification) {
406        getNotificationManager().notify(mSystemMessageNotificationId, notification);
407    }
408
409    private void handleConnectToNetworkAction() {
410        mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
411                ConnectToNetworkNotificationAndActionCount.ACTION_CONNECT_TO_NETWORK);
412        if (mState != STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
413            return;
414        }
415        postNotification(mNotificationBuilder.createNetworkConnectingNotification(mTag,
416                mRecommendedNetwork));
417        mWifiMetrics.incrementConnectToNetworkNotification(mTag,
418                ConnectToNetworkNotificationAndActionCount.NOTIFICATION_CONNECTING_TO_NETWORK);
419
420        Log.d(mTag,
421                "User initiated connection to recommended network: " + mRecommendedNetwork.SSID);
422        WifiConfiguration network = createRecommendedNetworkConfig(mRecommendedNetwork);
423        Message msg = Message.obtain();
424        msg.what = WifiManager.CONNECT_NETWORK;
425        msg.arg1 = WifiConfiguration.INVALID_NETWORK_ID;
426        msg.obj = network;
427        msg.replyTo = mSrcMessenger;
428        mWifiStateMachine.sendMessage(msg);
429
430        mState = STATE_CONNECTING_IN_NOTIFICATION;
431        mHandler.postDelayed(
432                () -> {
433                    if (mState == STATE_CONNECTING_IN_NOTIFICATION) {
434                        handleConnectionFailure();
435                    }
436                },
437                TIME_TO_SHOW_CONNECTING_MILLIS);
438    }
439
440    WifiConfiguration createRecommendedNetworkConfig(ScanResult recommendedNetwork) {
441        return ScanResultUtil.createNetworkFromScanResult(recommendedNetwork);
442    }
443
444    private void handleSeeAllNetworksAction() {
445        mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
446                ConnectToNetworkNotificationAndActionCount.ACTION_PICK_WIFI_NETWORK);
447        startWifiSettings();
448    }
449
450    private void startWifiSettings() {
451        // Close notification drawer before opening the picker.
452        mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
453        mContext.startActivity(
454                new Intent(Settings.ACTION_WIFI_SETTINGS)
455                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
456        clearPendingNotification(false /* resetRepeatTime */);
457    }
458
459    private void handleConnectionAttemptFailedToSend() {
460        handleConnectionFailure();
461        mWifiMetrics.incrementNumNetworkConnectMessageFailedToSend(mTag);
462    }
463
464    private void handlePickWifiNetworkAfterConnectFailure() {
465        mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
466                ConnectToNetworkNotificationAndActionCount
467                        .ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE);
468        startWifiSettings();
469    }
470
471    private void handleUserDismissedAction() {
472        Log.d(mTag, "User dismissed notification with state=" + mState);
473        mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
474                ConnectToNetworkNotificationAndActionCount.ACTION_USER_DISMISSED_NOTIFICATION);
475        if (mState == STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
476            // blacklist dismissed network
477            mBlacklistedSsids.add(mRecommendedNetwork.SSID);
478            mWifiMetrics.setNetworkRecommenderBlacklistSize(mTag, mBlacklistedSsids.size());
479            mConfigManager.saveToStore(false /* forceWrite */);
480            Log.d(mTag, "Network is added to the network notification blacklist: "
481                    + mRecommendedNetwork.SSID);
482        }
483        resetStateAndDelayNotification();
484    }
485
486    private void resetStateAndDelayNotification() {
487        mState = STATE_NO_NOTIFICATION;
488        mNotificationRepeatTime = System.currentTimeMillis() + mNotificationRepeatDelay;
489        mRecommendedNetwork = null;
490    }
491
492    /** Dump this network notifier's state. */
493    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
494        pw.println(mTag + ": ");
495        pw.println("mSettingEnabled " + mSettingEnabled);
496        pw.println("currentTime: " + mClock.getWallClockMillis());
497        pw.println("mNotificationRepeatTime: " + mNotificationRepeatTime);
498        pw.println("mState: " + mState);
499        pw.println("mBlacklistedSsids: " + mBlacklistedSsids.toString());
500    }
501
502    private class AvailableNetworkNotifierStoreData implements SsidSetStoreData.DataSource {
503        @Override
504        public Set<String> getSsids() {
505            return new ArraySet<>(mBlacklistedSsids);
506        }
507
508        @Override
509        public void setSsids(Set<String> ssidList) {
510            mBlacklistedSsids.addAll(ssidList);
511            mWifiMetrics.setNetworkRecommenderBlacklistSize(mTag, mBlacklistedSsids.size());
512        }
513    }
514
515    private class NotificationEnabledSettingObserver extends ContentObserver {
516        NotificationEnabledSettingObserver(Handler handler) {
517            super(handler);
518        }
519
520        public void register() {
521            mFrameworkFacade.registerContentObserver(mContext,
522                    Settings.Global.getUriFor(mToggleSettingsName), true, this);
523            mSettingEnabled = getValue();
524        }
525
526        @Override
527        public void onChange(boolean selfChange) {
528            super.onChange(selfChange);
529            mSettingEnabled = getValue();
530            clearPendingNotification(true /* resetRepeatTime */);
531        }
532
533        private boolean getValue() {
534            boolean enabled =
535                    mFrameworkFacade.getIntegerSetting(mContext, mToggleSettingsName, 1) == 1;
536            mWifiMetrics.setIsWifiNetworksAvailableNotificationEnabled(mTag, enabled);
537            Log.d(mTag, "Settings toggle enabled=" + enabled);
538            return enabled;
539        }
540    }
541}
542