• 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 static com.android.wifitrackerlib.Utils.getSecurityTypesFromScanResult;
20 
21 import android.app.Dialog;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.graphics.drawable.Drawable;
25 import android.net.wifi.ScanResult;
26 import android.net.wifi.WifiConfiguration;
27 import android.net.wifi.WifiManager.NetworkRequestUserSelectionCallback;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.os.HandlerThread;
31 import android.os.Looper;
32 import android.os.Process;
33 import android.os.SimpleClock;
34 import android.os.SystemClock;
35 import android.text.TextUtils;
36 import android.view.LayoutInflater;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.widget.ArrayAdapter;
40 import android.widget.BaseAdapter;
41 import android.widget.Button;
42 import android.widget.ProgressBar;
43 import android.widget.TextView;
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.overlay.FeatureFactory;
52 import com.android.settingslib.Utils;
53 import com.android.wifitrackerlib.WifiEntry;
54 import com.android.wifitrackerlib.WifiPickerTracker;
55 
56 import java.time.Clock;
57 import java.time.ZoneOffset;
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 NetworkRequestDialogBaseFragment implements
67         DialogInterface.OnClickListener, WifiPickerTracker.WifiPickerTrackerCallback {
68 
69     private static final String TAG = "NetworkRequestDialogFragment";
70 
71     /**
72      * Spec defines there should be 5 wifi ap on the list at most or just show all if {@code
73      * mShowLimitedItem} is false.
74      */
75     private static final int MAX_NUMBER_LIST_ITEM = 5;
76     private boolean mShowLimitedItem = true;
77 
78     private static class MatchWifi {
79         String mSsid;
80         List<Integer> mSecurityTypes;
81     }
82     private List<MatchWifi> mMatchWifis = new ArrayList<>();
83     @VisibleForTesting List<WifiEntry> mFilteredWifiEntries = new ArrayList<>();
84     private WifiEntryAdapter mDialogAdapter;
85     private NetworkRequestUserSelectionCallback mUserSelectionCallback;
86 
87     @VisibleForTesting WifiPickerTracker mWifiPickerTracker;
88     // Worker thread used for WifiPickerTracker work.
89     private HandlerThread mWorkerThread;
90     // Max age of tracked WifiEntries.
91     private static final long MAX_SCAN_AGE_MILLIS = 15_000;
92     // Interval between initiating WifiPickerTracker scans.
93     private static final long SCAN_INTERVAL_MILLIS = 10_000;
94 
newInstance()95     public static NetworkRequestDialogFragment newInstance() {
96         NetworkRequestDialogFragment dialogFragment = new NetworkRequestDialogFragment();
97         return dialogFragment;
98     }
99 
100     @Override
onCreate(Bundle savedInstanceState)101     public void onCreate(Bundle savedInstanceState) {
102         super.onCreate(savedInstanceState);
103 
104         mWorkerThread = new HandlerThread(
105                 TAG + "{" + Integer.toHexString(System.identityHashCode(this)) + "}",
106                 Process.THREAD_PRIORITY_BACKGROUND);
107         mWorkerThread.start();
108         final Clock elapsedRealtimeClock = new SimpleClock(ZoneOffset.UTC) {
109             @Override
110             public long millis() {
111                 return SystemClock.elapsedRealtime();
112             }
113         };
114         final Context context = getContext();
115         mWifiPickerTracker = FeatureFactory.getFactory(context)
116                 .getWifiTrackerLibProvider()
117                 .createWifiPickerTracker(getSettingsLifecycle(), context,
118                         new Handler(Looper.getMainLooper()),
119                         mWorkerThread.getThreadHandler(),
120                         elapsedRealtimeClock,
121                         MAX_SCAN_AGE_MILLIS,
122                         SCAN_INTERVAL_MILLIS,
123                         this);
124     }
125 
126     @Override
onCreateDialog(Bundle savedInstanceState)127     public Dialog onCreateDialog(Bundle savedInstanceState) {
128         final Context context = getContext();
129 
130         // Prepares title.
131         final LayoutInflater inflater = LayoutInflater.from(context);
132         final View customTitle = inflater.inflate(R.layout.network_request_dialog_title, null);
133 
134         final TextView title = customTitle.findViewById(R.id.network_request_title_text);
135         title.setText(getTitle());
136         final TextView summary = customTitle.findViewById(R.id.network_request_summary_text);
137         summary.setText(getSummary());
138 
139         final ProgressBar progressBar = customTitle.findViewById(
140                 R.id.network_request_title_progress);
141         progressBar.setVisibility(View.VISIBLE);
142 
143         // Prepares adapter.
144         mDialogAdapter = new WifiEntryAdapter(context,
145                 R.layout.preference_access_point, mFilteredWifiEntries);
146 
147         final AlertDialog.Builder builder = new AlertDialog.Builder(context)
148                 .setCustomTitle(customTitle)
149                 .setAdapter(mDialogAdapter, this)
150                 .setNegativeButton(R.string.cancel, (dialog, which) -> onCancel(dialog))
151                 // Do nothings, will replace the onClickListener to avoid auto closing dialog.
152                 .setNeutralButton(R.string.network_connection_request_dialog_showall,
153                         null /* OnClickListener */);
154 
155         // Clicking list item is to connect wifi ap.
156         final AlertDialog dialog = builder.create();
157         dialog.getListView().setOnItemClickListener(
158                 (parent, view, position, id) -> this.onClick(dialog, position));
159 
160         // Don't dismiss dialog when touching outside. User reports it is easy to touch outside.
161         // This causes dialog to close.
162         setCancelable(false);
163 
164         dialog.setOnShowListener((dialogInterface) -> {
165             // Replace NeutralButton onClickListener to avoid closing dialog
166             final Button neutralBtn = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
167             neutralBtn.setVisibility(View.GONE);
168             neutralBtn.setOnClickListener(v -> {
169                 mShowLimitedItem = false;
170                 updateWifiEntries();
171                 updateUi();
172                 neutralBtn.setVisibility(View.GONE);
173             });
174         });
175         return dialog;
176     }
177 
getDialogAdapter()178     private BaseAdapter getDialogAdapter() {
179         return mDialogAdapter;
180     }
181 
182     @Override
onClick(DialogInterface dialog, int which)183     public void onClick(DialogInterface dialog, int which) {
184         if (mFilteredWifiEntries.size() == 0 || which >= mFilteredWifiEntries.size()) {
185             return;  // Invalid values.
186         }
187         if (mUserSelectionCallback == null) {
188             return; // Callback is missing or not ready.
189         }
190 
191         final WifiEntry wifiEntry = mFilteredWifiEntries.get(which);
192         WifiConfiguration config = wifiEntry.getWifiConfiguration();
193         if (config == null) {
194             config = WifiUtils.getWifiConfig(wifiEntry, null /* scanResult */);
195         }
196         mUserSelectionCallback.select(config);
197     }
198 
199     @Override
onCancel(@onNull DialogInterface dialog)200     public void onCancel(@NonNull DialogInterface dialog) {
201         super.onCancel(dialog);
202 
203         if (mUserSelectionCallback != null) {
204             mUserSelectionCallback.reject();
205         }
206     }
207 
208     @Override
onDestroy()209     public void onDestroy() {
210         mWorkerThread.quit();
211 
212         super.onDestroy();
213     }
214 
showAllButton()215     private void showAllButton() {
216         final AlertDialog alertDialog = (AlertDialog) getDialog();
217         if (alertDialog == null) {
218             return;
219         }
220 
221         final Button neutralBtn = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL);
222         if (neutralBtn != null) {
223             neutralBtn.setVisibility(View.VISIBLE);
224         }
225     }
226 
hideProgressIcon()227     private void hideProgressIcon() {
228         final AlertDialog alertDialog = (AlertDialog) getDialog();
229         if (alertDialog == null) {
230             return;
231         }
232 
233         final View progress = alertDialog.findViewById(R.id.network_request_title_progress);
234         if (progress != null) {
235             progress.setVisibility(View.GONE);
236         }
237     }
238 
239     /** Called when the state of Wifi has changed. */
240     @Override
onWifiStateChanged()241     public void onWifiStateChanged() {
242         if (mMatchWifis.size() == 0) {
243             return;
244         }
245         updateWifiEntries();
246         updateUi();
247     }
248 
249     /**
250      * Update the results when data changes
251      */
252     @Override
onWifiEntriesChanged()253     public void onWifiEntriesChanged() {
254         if (mMatchWifis.size() == 0) {
255             return;
256         }
257         updateWifiEntries();
258         updateUi();
259     }
260 
261     @Override
onNumSavedSubscriptionsChanged()262     public void onNumSavedSubscriptionsChanged() {
263         // Do nothing.
264     }
265 
266     @Override
onNumSavedNetworksChanged()267     public void onNumSavedNetworksChanged() {
268         // Do nothing.
269     }
270 
271     @VisibleForTesting
updateWifiEntries()272     void updateWifiEntries() {
273         final List<WifiEntry> wifiEntries = new ArrayList<>();
274         WifiEntry connectedWifiEntry = mWifiPickerTracker.getConnectedWifiEntry();
275         String connectedSsid;
276         if (connectedWifiEntry != null) {
277             connectedSsid = connectedWifiEntry.getSsid();
278             wifiEntries.add(connectedWifiEntry);
279         } else {
280             connectedSsid = null;
281         }
282         wifiEntries.addAll(mWifiPickerTracker.getWifiEntries());
283 
284         mFilteredWifiEntries.clear();
285         mFilteredWifiEntries.addAll(wifiEntries.stream()
286                 .filter(entry -> isMatchedWifiEntry(entry, connectedSsid))
287                 .limit(mShowLimitedItem ? MAX_NUMBER_LIST_ITEM : Long.MAX_VALUE)
288                 .toList());
289     }
290 
isMatchedWifiEntry(WifiEntry entry, String connectedSsid)291     private boolean isMatchedWifiEntry(WifiEntry entry, String connectedSsid) {
292         if (entry.getConnectedState() == WifiEntry.CONNECTED_STATE_DISCONNECTED
293                 && TextUtils.equals(entry.getSsid(), connectedSsid)) {
294             // WifiPickerTracker may return a duplicate unsaved network that is separate from
295             // the connecting app-requested network, so make sure we only show the connected
296             // app-requested one.
297             return false;
298         }
299         for (MatchWifi wifi : mMatchWifis) {
300             if (!TextUtils.equals(entry.getSsid(), wifi.mSsid)) {
301                 continue;
302             }
303             for (Integer security : wifi.mSecurityTypes) {
304                 if (entry.getSecurityTypes().contains(security)) {
305                     return true;
306                 }
307             }
308         }
309         return false;
310     }
311 
312     private class WifiEntryAdapter extends ArrayAdapter<WifiEntry> {
313 
314         private final int mResourceId;
315         private final LayoutInflater mInflater;
316 
WifiEntryAdapter(Context context, int resourceId, List<WifiEntry> objects)317         WifiEntryAdapter(Context context, int resourceId, List<WifiEntry> objects) {
318             super(context, resourceId, objects);
319             mResourceId = resourceId;
320             mInflater = LayoutInflater.from(context);
321         }
322 
323         @Override
getView(int position, View view, ViewGroup parent)324         public View getView(int position, View view, ViewGroup parent) {
325             if (view == null) {
326                 view = mInflater.inflate(mResourceId, parent, false);
327 
328                 final View divider = view.findViewById(
329                         com.android.settingslib.R.id.two_target_divider);
330                 divider.setVisibility(View.GONE);
331             }
332 
333             final WifiEntry wifiEntry = getItem(position);
334 
335             final TextView titleView = view.findViewById(android.R.id.title);
336             if (titleView != null) {
337                 // Shows whole SSID for better UX.
338                 titleView.setSingleLine(false);
339                 titleView.setText(wifiEntry.getTitle());
340             }
341 
342             final TextView summary = view.findViewById(android.R.id.summary);
343             if (summary != null) {
344                 final String summaryString = wifiEntry.getSummary();
345                 if (TextUtils.isEmpty(summaryString)) {
346                     summary.setVisibility(View.GONE);
347                 } else {
348                     summary.setVisibility(View.VISIBLE);
349                     summary.setText(summaryString);
350                 }
351             }
352 
353             final PreferenceImageView imageView = view.findViewById(android.R.id.icon);
354             final int level = wifiEntry.getLevel();
355             if (imageView != null && level != WifiEntry.WIFI_LEVEL_UNREACHABLE) {
356                 final Drawable drawable = getContext().getDrawable(
357                         Utils.getWifiIconResource(level));
358                 drawable.setTintList(
359                         Utils.getColorAttr(getContext(), android.R.attr.colorControlNormal));
360                 imageView.setImageDrawable(drawable);
361             }
362 
363             return view;
364         }
365     }
366 
367     @Override
onUserSelectionCallbackRegistration( NetworkRequestUserSelectionCallback userSelectionCallback)368     public void onUserSelectionCallbackRegistration(
369             NetworkRequestUserSelectionCallback userSelectionCallback) {
370         mUserSelectionCallback = userSelectionCallback;
371     }
372 
373     @Override
onMatch(List<ScanResult> scanResults)374     public void onMatch(List<ScanResult> scanResults) {
375         mMatchWifis.clear();
376         for (ScanResult scanResult : scanResults) {
377             MatchWifi matchWifi = new MatchWifi();
378             matchWifi.mSsid = scanResult.SSID;
379             matchWifi.mSecurityTypes = getSecurityTypesFromScanResult(scanResult);
380             mMatchWifis.add(matchWifi);
381         }
382 
383         updateWifiEntries();
384         updateUi();
385     }
386 
387     @VisibleForTesting
updateUi()388     void updateUi() {
389         // Update related UI buttons
390         if (mShowLimitedItem && mFilteredWifiEntries.size() >= MAX_NUMBER_LIST_ITEM) {
391             showAllButton();
392         }
393         if (mFilteredWifiEntries.size() > 0) {
394             hideProgressIcon();
395         }
396 
397         if (getDialogAdapter() != null) {
398             getDialogAdapter().notifyDataSetChanged();
399         }
400     }
401 }
402