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