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