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