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