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