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