• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.settings.wifi;
18 
19 import android.app.Activity;
20 import android.app.Dialog;
21 import android.app.settings.SettingsEnums;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.content.Intent;
25 import android.graphics.drawable.Drawable;
26 import android.net.wifi.ScanResult;
27 import android.net.wifi.WifiConfiguration;
28 import android.net.wifi.WifiManager;
29 import android.net.wifi.WifiManager.NetworkRequestMatchCallback;
30 import android.net.wifi.WifiManager.NetworkRequestUserSelectionCallback;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.os.Message;
34 import android.text.TextUtils;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.widget.ArrayAdapter;
39 import android.widget.BaseAdapter;
40 import android.widget.Button;
41 import android.widget.ProgressBar;
42 import android.widget.TextView;
43 import android.widget.Toast;
44 
45 import androidx.annotation.NonNull;
46 import androidx.annotation.VisibleForTesting;
47 import androidx.appcompat.app.AlertDialog;
48 import androidx.preference.internal.PreferenceImageView;
49 
50 import com.android.settings.R;
51 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
52 import com.android.settings.wifi.NetworkRequestErrorDialogFragment.ERROR_DIALOG_TYPE;
53 import com.android.settingslib.Utils;
54 import com.android.settingslib.core.lifecycle.Lifecycle;
55 import com.android.settingslib.wifi.AccessPoint;
56 import com.android.settingslib.wifi.WifiTracker;
57 import com.android.settingslib.wifi.WifiTrackerFactory;
58 
59 import java.util.ArrayList;
60 import java.util.List;
61 
62 /**
63  * The Fragment sets up callback {@link NetworkRequestMatchCallback} with framework. To handle most
64  * behaviors of the callback when requesting wifi network, except for error message. When error
65  * happens, {@link NetworkRequestErrorDialogFragment} will be called to display error message.
66  */
67 public class NetworkRequestDialogFragment extends InstrumentedDialogFragment implements
68         DialogInterface.OnClickListener, NetworkRequestMatchCallback {
69 
70     /** Message sent to us to stop scanning wifi and pop up timeout dialog. */
71     private static final int MESSAGE_STOP_SCAN_WIFI_LIST = 0;
72 
73     /**
74      * Spec defines there should be 5 wifi ap on the list at most or just show all if {@code
75      * mShowLimitedItem} is false.
76      */
77     private static final int MAX_NUMBER_LIST_ITEM = 5;
78     private boolean mShowLimitedItem = true;
79 
80     /** Delayed time to stop scanning wifi. */
81     private static final int DELAY_TIME_STOP_SCAN_MS = 30 * 1000;
82 
83     @VisibleForTesting
84     final static String EXTRA_APP_NAME = "com.android.settings.wifi.extra.APP_NAME";
85     final static String EXTRA_IS_SPECIFIED_SSID =
86             "com.android.settings.wifi.extra.REQUEST_IS_FOR_SINGLE_NETWORK";
87 
88     private List<AccessPoint> mAccessPointList;
89     private FilterWifiTracker mFilterWifiTracker;
90     private AccessPointAdapter mDialogAdapter;
91     private NetworkRequestUserSelectionCallback mUserSelectionCallback;
92     private boolean mIsSpecifiedSsid;
93     private boolean mWaitingConnectCallback;
94 
newInstance()95     public static NetworkRequestDialogFragment newInstance() {
96         NetworkRequestDialogFragment dialogFragment = new NetworkRequestDialogFragment();
97         return dialogFragment;
98     }
99 
100     @Override
onCreateDialog(Bundle savedInstanceState)101     public Dialog onCreateDialog(Bundle savedInstanceState) {
102         final Context context = getContext();
103 
104         // Prepares title.
105         final LayoutInflater inflater = LayoutInflater.from(context);
106         final View customTitle = inflater.inflate(R.layout.network_request_dialog_title, null);
107 
108         final TextView title = customTitle.findViewById(R.id.network_request_title_text);
109         title.setText(getTitle());
110 
111         final Intent intent = getActivity().getIntent();
112         if (intent != null) {
113             mIsSpecifiedSsid = intent.getBooleanExtra(EXTRA_IS_SPECIFIED_SSID, false);
114         }
115 
116         final ProgressBar progressBar = customTitle.findViewById(
117                 R.id.network_request_title_progress);
118         progressBar.setVisibility(View.VISIBLE);
119 
120         // Prepares adapter.
121         mDialogAdapter = new AccessPointAdapter(context,
122                 R.layout.preference_access_point, getAccessPointList());
123 
124         final AlertDialog.Builder builder = new AlertDialog.Builder(context)
125                 .setCustomTitle(customTitle)
126                 .setAdapter(mDialogAdapter, this)
127                 .setNegativeButton(R.string.cancel, (dialog, which) -> onCancel(dialog))
128                 // Do nothings, will replace the onClickListener to avoid auto closing dialog.
129                 .setNeutralButton(R.string.network_connection_request_dialog_showall,
130                         null /* OnClickListener */);
131         if (mIsSpecifiedSsid) {
132             builder.setPositiveButton(R.string.wifi_connect, null /* OnClickListener */);
133         }
134 
135         // Clicking list item is to connect wifi ap.
136         final AlertDialog dialog = builder.create();
137         dialog.getListView()
138                 .setOnItemClickListener(
139                         (parent, view, position, id) -> this.onClick(dialog, position));
140 
141         // Don't dismiss dialog when touching outside. User reports it is easy to touch outside.
142         // This causes dialog to close.
143         setCancelable(false);
144 
145         dialog.setOnShowListener((dialogInterface) -> {
146             // Replace NeutralButton onClickListener to avoid closing dialog
147             final Button neutralBtn = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
148             neutralBtn.setVisibility(View.GONE);
149             neutralBtn.setOnClickListener(v -> {
150                 mShowLimitedItem = false;
151                 renewAccessPointList(null /* List<ScanResult> */);
152                 notifyAdapterRefresh();
153                 neutralBtn.setVisibility(View.GONE);
154             });
155 
156             // Replace Positive onClickListener to avoid closing dialog
157             if (mIsSpecifiedSsid) {
158                 final Button positiveBtn = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
159                 positiveBtn.setOnClickListener(v -> {
160                     // When clicking connect button, should connect to the first and the only one
161                     // list item.
162                     this.onClick(dialog, 0 /* position */);
163                 });
164                 // Disable button in first, and enable it after there are some accesspoints in list.
165                 positiveBtn.setEnabled(false);
166             }
167         });
168         return dialog;
169     }
170 
getTitle()171     private String getTitle() {
172         final Intent intent = getActivity().getIntent();
173         String appName = "";
174         if (intent != null) {
175             appName = intent.getStringExtra(EXTRA_APP_NAME);
176         }
177 
178         return getString(R.string.network_connection_request_dialog_title, appName);
179     }
180 
181     @NonNull
getAccessPointList()182     List<AccessPoint> getAccessPointList() {
183         // Initials list for adapter, in case of display crashing.
184         if (mAccessPointList == null) {
185             mAccessPointList = new ArrayList<>();
186         }
187         return mAccessPointList;
188     }
189 
getDialogAdapter()190     private BaseAdapter getDialogAdapter() {
191         return mDialogAdapter;
192     }
193 
194     @Override
onClick(DialogInterface dialog, int which)195     public void onClick(DialogInterface dialog, int which) {
196         final List<AccessPoint> accessPointList = getAccessPointList();
197         if (accessPointList.size() == 0) {
198             return;  // Invalid values.
199         }
200         if (mUserSelectionCallback == null) {
201             return; // Callback is missing or not ready.
202         }
203 
204         if (which < accessPointList.size()) {
205             final AccessPoint selectedAccessPoint = accessPointList.get(which);
206             WifiConfiguration wifiConfig = selectedAccessPoint.getConfig();
207             if (wifiConfig == null) {
208                 wifiConfig = WifiUtils.getWifiConfig(selectedAccessPoint, /* scanResult */
209                         null, /* password */ null);
210             }
211 
212             if (wifiConfig != null) {
213                 mUserSelectionCallback.select(wifiConfig);
214 
215                 mWaitingConnectCallback = true;
216                 updateConnectButton(false);
217             }
218         }
219     }
220 
221     @Override
onCancel(@onNull DialogInterface dialog)222     public void onCancel(@NonNull DialogInterface dialog) {
223         super.onCancel(dialog);
224         // Finishes the activity when user clicks back key or outside of the dialog.
225         if (getActivity() != null) {
226             getActivity().finish();
227         }
228         if (mUserSelectionCallback != null) {
229             mUserSelectionCallback.reject();
230         }
231     }
232 
233     @Override
onPause()234     public void onPause() {
235         super.onPause();
236 
237         mHandler.removeMessages(MESSAGE_STOP_SCAN_WIFI_LIST);
238         final WifiManager wifiManager = getContext().getApplicationContext()
239                 .getSystemService(WifiManager.class);
240         if (wifiManager != null) {
241             wifiManager.unregisterNetworkRequestMatchCallback(this);
242         }
243 
244         if (mFilterWifiTracker != null) {
245             mFilterWifiTracker.onPause();
246         }
247     }
248 
249     @Override
onDestroy()250     public void onDestroy() {
251         super.onDestroy();
252 
253         if (mFilterWifiTracker != null) {
254             mFilterWifiTracker.onDestroy();
255             mFilterWifiTracker = null;
256         }
257     }
258 
showAllButton()259     private void showAllButton() {
260         final AlertDialog alertDialog = (AlertDialog) getDialog();
261         if (alertDialog == null) {
262             return;
263         }
264 
265         final Button neutralBtn = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL);
266         if (neutralBtn != null) {
267             neutralBtn.setVisibility(View.VISIBLE);
268         }
269     }
270 
updateConnectButton(boolean enabled)271     private void updateConnectButton(boolean enabled) {
272         // The button is only showed in single SSID mode.
273         if (!mIsSpecifiedSsid) {
274             return;
275         }
276 
277         final AlertDialog alertDialog = (AlertDialog) getDialog();
278         if (alertDialog == null) {
279             return;
280         }
281 
282         final Button positiveBtn = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
283         if (positiveBtn != null) {
284             positiveBtn.setEnabled(enabled);
285         }
286     }
287 
hideProgressIcon()288     private void hideProgressIcon() {
289         final AlertDialog alertDialog = (AlertDialog) getDialog();
290         if (alertDialog == null) {
291             return;
292         }
293 
294         final View progress = alertDialog.findViewById(R.id.network_request_title_progress);
295         if (progress != null) {
296             progress.setVisibility(View.GONE);
297         }
298     }
299 
300     @Override
onResume()301     public void onResume() {
302         super.onResume();
303 
304         final WifiManager wifiManager = getContext().getApplicationContext()
305                 .getSystemService(WifiManager.class);
306         if (wifiManager != null) {
307             wifiManager.registerNetworkRequestMatchCallback(this, mHandler);
308         }
309         // Sets time-out to stop scanning.
310         mHandler.sendEmptyMessageDelayed(MESSAGE_STOP_SCAN_WIFI_LIST, DELAY_TIME_STOP_SCAN_MS);
311 
312         if (mFilterWifiTracker == null) {
313             mFilterWifiTracker = new FilterWifiTracker(getActivity(), getSettingsLifecycle());
314         }
315         mFilterWifiTracker.onResume();
316     }
317 
318     private final Handler mHandler = new Handler() {
319         @Override
320         public void handleMessage(Message msg) {
321             switch (msg.what) {
322                 case MESSAGE_STOP_SCAN_WIFI_LIST:
323                     removeMessages(MESSAGE_STOP_SCAN_WIFI_LIST);
324                     stopScanningAndPopErrorDialog(ERROR_DIALOG_TYPE.TIME_OUT);
325                     break;
326                 default:
327                     // Do nothing.
328                     break;
329             }
330         }
331     };
332 
stopScanningAndPopErrorDialog(ERROR_DIALOG_TYPE type)333     protected void stopScanningAndPopErrorDialog(ERROR_DIALOG_TYPE type) {
334         // Dismisses current dialog.
335         final Dialog dialog =  getDialog();
336         if (dialog != null && dialog.isShowing()) {
337             dismiss();
338         }
339 
340         // Throws error dialog.
341         final NetworkRequestErrorDialogFragment fragment = NetworkRequestErrorDialogFragment
342                 .newInstance();
343         final Bundle bundle = new Bundle();
344         bundle.putSerializable(NetworkRequestErrorDialogFragment.DIALOG_TYPE, type);
345         fragment.setArguments(bundle);
346         fragment.show(getActivity().getSupportFragmentManager(),
347                 NetworkRequestDialogFragment.class.getSimpleName());
348     }
349 
350     @Override
getMetricsCategory()351     public int getMetricsCategory() {
352         return SettingsEnums.WIFI_SCANNING_NEEDED_DIALOG;
353     }
354 
355     private class AccessPointAdapter extends ArrayAdapter<AccessPoint> {
356 
357         private final int mResourceId;
358         private final LayoutInflater mInflater;
359 
AccessPointAdapter(Context context, int resourceId, List<AccessPoint> objects)360         public AccessPointAdapter(Context context, int resourceId, List<AccessPoint> objects) {
361             super(context, resourceId, objects);
362             mResourceId = resourceId;
363             mInflater = LayoutInflater.from(context);
364         }
365 
366         @Override
getView(int position, View view, ViewGroup parent)367         public View getView(int position, View view, ViewGroup parent) {
368             if (view == null) {
369                 view = mInflater.inflate(mResourceId, parent, false);
370 
371                 final View divider = view.findViewById(
372                         com.android.settingslib.R.id.two_target_divider);
373                 divider.setVisibility(View.GONE);
374             }
375 
376             final AccessPoint accessPoint = getItem(position);
377 
378             final TextView titleView = view.findViewById(android.R.id.title);
379             if (titleView != null) {
380                 // Shows whole SSID for better UX.
381                 titleView.setSingleLine(false);
382                 titleView.setText(accessPoint.getTitle());
383             }
384 
385             final TextView summary = view.findViewById(android.R.id.summary);
386             if (summary != null) {
387                 final String summaryString = accessPoint.getSettingsSummary();
388                 if (TextUtils.isEmpty(summaryString)) {
389                     summary.setVisibility(View.GONE);
390                 } else {
391                     summary.setVisibility(View.VISIBLE);
392                     summary.setText(summaryString);
393                 }
394             }
395 
396             final PreferenceImageView imageView = view.findViewById(android.R.id.icon);
397             final int level = accessPoint.getLevel();
398             if (imageView != null) {
399                 final Drawable drawable = getContext().getDrawable(
400                         Utils.getWifiIconResource(level));
401                 drawable.setTintList(
402                         Utils.getColorAttr(getContext(), android.R.attr.colorControlNormal));
403                 imageView.setImageDrawable(drawable);
404             }
405 
406             return view;
407         }
408     }
409 
410     @Override
onAbort()411     public void onAbort() {
412         stopScanningAndPopErrorDialog(ERROR_DIALOG_TYPE.ABORT);
413     }
414 
415     @Override
onUserSelectionCallbackRegistration( NetworkRequestUserSelectionCallback userSelectionCallback)416     public void onUserSelectionCallbackRegistration(
417             NetworkRequestUserSelectionCallback userSelectionCallback) {
418         mUserSelectionCallback = userSelectionCallback;
419     }
420 
421     @Override
onMatch(List<ScanResult> scanResults)422     public void onMatch(List<ScanResult> scanResults) {
423         // Shouldn't need to renew cached list, since input result is empty.
424         if (scanResults != null && scanResults.size() > 0) {
425             mHandler.removeMessages(MESSAGE_STOP_SCAN_WIFI_LIST);
426             renewAccessPointList(scanResults);
427 
428             notifyAdapterRefresh();
429         }
430     }
431 
432     // Updates internal AccessPoint list from WifiTracker. scanResults are used to update key list
433     // of AccessPoint, and could be null if there is no necessary to update key list.
renewAccessPointList(List<ScanResult> scanResults)434     private void renewAccessPointList(List<ScanResult> scanResults) {
435         if (mFilterWifiTracker == null) {
436             return;
437         }
438 
439         // TODO(b/119846365): Checks if we could escalate the converting effort.
440         // Updates keys of scanResults into FilterWifiTracker for updating matched AccessPoints.
441         if (scanResults != null) {
442             mFilterWifiTracker.updateKeys(scanResults);
443         }
444 
445         // Re-gets matched AccessPoints from WifiTracker.
446         final List<AccessPoint> list = getAccessPointList();
447         list.clear();
448         list.addAll(mFilterWifiTracker.getAccessPoints());
449     }
450 
451     @VisibleForTesting
notifyAdapterRefresh()452     void notifyAdapterRefresh() {
453         if (getDialogAdapter() != null) {
454             getDialogAdapter().notifyDataSetChanged();
455         }
456     }
457 
458     @Override
onUserSelectionConnectSuccess(WifiConfiguration wificonfiguration)459     public void onUserSelectionConnectSuccess(WifiConfiguration wificonfiguration) {
460         final Activity activity = getActivity();
461         if (activity != null) {
462             Toast.makeText(activity, R.string.network_connection_connect_successful,
463                     Toast.LENGTH_SHORT).show();
464             activity.finish();
465         }
466     }
467 
468     @Override
onUserSelectionConnectFailure(WifiConfiguration wificonfiguration)469     public void onUserSelectionConnectFailure(WifiConfiguration wificonfiguration) {
470         // Do nothing when selection is failed, let user could try again easily.
471         mWaitingConnectCallback = false;
472         updateConnectButton(true);
473     }
474 
475     private final class FilterWifiTracker {
476         private final List<String> mAccessPointKeys;
477         private final WifiTracker mWifiTracker;
478 
FilterWifiTracker(Context context, Lifecycle lifecycle)479         public FilterWifiTracker(Context context, Lifecycle lifecycle) {
480             mWifiTracker = WifiTrackerFactory.create(context, mWifiListener,
481                     lifecycle, /* includeSaved */ true, /* includeScans */ true);
482             mAccessPointKeys = new ArrayList<>();
483         }
484 
485         /**
486          * Updates key list from input. {@code onMatch()} may be called in multi-times according
487          * wifi scanning result, so needs patchwork here.
488          */
updateKeys(List<ScanResult> scanResults)489         public void updateKeys(List<ScanResult> scanResults) {
490             for (ScanResult scanResult : scanResults) {
491                 final String key = AccessPoint.getKey(scanResult);
492                 if (!mAccessPointKeys.contains(key)) {
493                     mAccessPointKeys.add(key);
494                 }
495             }
496         }
497 
498         /**
499          * Returns only AccessPoints whose key is in {@code mAccessPointKeys}.
500          *
501          * @return List of matched AccessPoints.
502          */
getAccessPoints()503         public List<AccessPoint> getAccessPoints() {
504             final List<AccessPoint> allAccessPoints = mWifiTracker.getAccessPoints();
505             final List<AccessPoint> result = new ArrayList<>();
506 
507             // The order should be kept, because order means wifi score (sorting in WifiTracker).
508             int count = 0;
509             for (AccessPoint accessPoint : allAccessPoints) {
510                 final String key = accessPoint.getKey();
511                 if (mAccessPointKeys.contains(key)) {
512                     result.add(accessPoint);
513 
514                     count++;
515                     // Limits how many count of items could show.
516                     if (mShowLimitedItem && count >= MAX_NUMBER_LIST_ITEM) {
517                         break;
518                     }
519                 }
520             }
521 
522             // Update related UI buttons
523             if (mShowLimitedItem && (count >= MAX_NUMBER_LIST_ITEM)) {
524                 showAllButton();
525             }
526             if (count > 0) {
527                 hideProgressIcon();
528             }
529             // Enable connect button if there is Accesspoint item, except for the situation that
530             // user click but connected status doesn't come back yet.
531             if (count < 0) {
532                 updateConnectButton(false);
533             } else if (!mWaitingConnectCallback) {
534                 updateConnectButton(true);
535             }
536 
537             return result;
538         }
539 
540         private WifiTracker.WifiListener mWifiListener = new WifiTracker.WifiListener() {
541 
542             @Override
543             public void onWifiStateChanged(int state) {
544                 notifyAdapterRefresh();
545             }
546 
547             @Override
548             public void onConnectedChanged() {
549                 notifyAdapterRefresh();
550             }
551 
552             @Override
553             public void onAccessPointsChanged() {
554                 notifyAdapterRefresh();
555             }
556         };
557 
onDestroy()558         public void onDestroy() {
559             if (mWifiTracker != null) {
560                 mWifiTracker.onDestroy();
561             }
562         }
563 
onResume()564         public void onResume() {
565             if (mWifiTracker != null) {
566                 mWifiTracker.onStart();
567             }
568         }
569 
onPause()570         public void onPause() {
571             if (mWifiTracker != null) {
572                 mWifiTracker.onStop();
573             }
574         }
575     }
576 }
577