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