Files
app_Settings/src/com/android/settings/wifi/NetworkRequestDialogFragment.java
Quang Anh Luong ae485a6774 Fix duplicate network request entry during connection
When connecting to a network request, a new NetworkRequestEntry is
created by WifiPickerTracker alongside the unsaved regular network,
which results in two of the same network appearing during the L2
connection phase. To fix this, filter out any disconnected networks that
have the same SSID and security type as the currently connecting
network.

Bug: 292064115
Test: manually verify no duplicate network appears when connecting to
network request

Change-Id: I3d6abb0d0a1e4eeab1b8fa396660851ba090c611
2023-07-31 14:51:07 +09:00

402 lines
14 KiB
Java

/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.wifi;
import static com.android.wifitrackerlib.Utils.getSecurityTypesFromScanResult;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager.NetworkRequestUserSelectionCallback;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Process;
import android.os.SimpleClock;
import android.os.SystemClock;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.internal.PreferenceImageView;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.Utils;
import com.android.wifitrackerlib.WifiEntry;
import com.android.wifitrackerlib.WifiPickerTracker;
import java.time.Clock;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
/**
* The Fragment sets up callback {@link NetworkRequestMatchCallback} with framework. To handle most
* behaviors of the callback when requesting wifi network, except for error message. When error
* happens, {@link NetworkRequestErrorDialogFragment} will be called to display error message.
*/
public class NetworkRequestDialogFragment extends NetworkRequestDialogBaseFragment implements
DialogInterface.OnClickListener, WifiPickerTracker.WifiPickerTrackerCallback {
private static final String TAG = "NetworkRequestDialogFragment";
/**
* Spec defines there should be 5 wifi ap on the list at most or just show all if {@code
* mShowLimitedItem} is false.
*/
private static final int MAX_NUMBER_LIST_ITEM = 5;
private boolean mShowLimitedItem = true;
private static class MatchWifi {
String mSsid;
List<Integer> mSecurityTypes;
}
private List<MatchWifi> mMatchWifis = new ArrayList<>();
@VisibleForTesting List<WifiEntry> mFilteredWifiEntries = new ArrayList<>();
private WifiEntryAdapter mDialogAdapter;
private NetworkRequestUserSelectionCallback mUserSelectionCallback;
@VisibleForTesting WifiPickerTracker mWifiPickerTracker;
// Worker thread used for WifiPickerTracker work.
private HandlerThread mWorkerThread;
// Max age of tracked WifiEntries.
private static final long MAX_SCAN_AGE_MILLIS = 15_000;
// Interval between initiating WifiPickerTracker scans.
private static final long SCAN_INTERVAL_MILLIS = 10_000;
public static NetworkRequestDialogFragment newInstance() {
NetworkRequestDialogFragment dialogFragment = new NetworkRequestDialogFragment();
return dialogFragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mWorkerThread = new HandlerThread(
TAG + "{" + Integer.toHexString(System.identityHashCode(this)) + "}",
Process.THREAD_PRIORITY_BACKGROUND);
mWorkerThread.start();
final Clock elapsedRealtimeClock = new SimpleClock(ZoneOffset.UTC) {
@Override
public long millis() {
return SystemClock.elapsedRealtime();
}
};
final Context context = getContext();
mWifiPickerTracker = FeatureFactory.getFactory(context)
.getWifiTrackerLibProvider()
.createWifiPickerTracker(getSettingsLifecycle(), context,
new Handler(Looper.getMainLooper()),
mWorkerThread.getThreadHandler(),
elapsedRealtimeClock,
MAX_SCAN_AGE_MILLIS,
SCAN_INTERVAL_MILLIS,
this);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Context context = getContext();
// Prepares title.
final LayoutInflater inflater = LayoutInflater.from(context);
final View customTitle = inflater.inflate(R.layout.network_request_dialog_title, null);
final TextView title = customTitle.findViewById(R.id.network_request_title_text);
title.setText(getTitle());
final TextView summary = customTitle.findViewById(R.id.network_request_summary_text);
summary.setText(getSummary());
final ProgressBar progressBar = customTitle.findViewById(
R.id.network_request_title_progress);
progressBar.setVisibility(View.VISIBLE);
// Prepares adapter.
mDialogAdapter = new WifiEntryAdapter(context,
R.layout.preference_access_point, mFilteredWifiEntries);
final AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setCustomTitle(customTitle)
.setAdapter(mDialogAdapter, this)
.setNegativeButton(R.string.cancel, (dialog, which) -> onCancel(dialog))
// Do nothings, will replace the onClickListener to avoid auto closing dialog.
.setNeutralButton(R.string.network_connection_request_dialog_showall,
null /* OnClickListener */);
// Clicking list item is to connect wifi ap.
final AlertDialog dialog = builder.create();
dialog.getListView().setOnItemClickListener(
(parent, view, position, id) -> this.onClick(dialog, position));
// Don't dismiss dialog when touching outside. User reports it is easy to touch outside.
// This causes dialog to close.
setCancelable(false);
dialog.setOnShowListener((dialogInterface) -> {
// Replace NeutralButton onClickListener to avoid closing dialog
final Button neutralBtn = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
neutralBtn.setVisibility(View.GONE);
neutralBtn.setOnClickListener(v -> {
mShowLimitedItem = false;
updateWifiEntries();
updateUi();
neutralBtn.setVisibility(View.GONE);
});
});
return dialog;
}
private BaseAdapter getDialogAdapter() {
return mDialogAdapter;
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (mFilteredWifiEntries.size() == 0 || which >= mFilteredWifiEntries.size()) {
return; // Invalid values.
}
if (mUserSelectionCallback == null) {
return; // Callback is missing or not ready.
}
final WifiEntry wifiEntry = mFilteredWifiEntries.get(which);
WifiConfiguration config = wifiEntry.getWifiConfiguration();
if (config == null) {
config = WifiUtils.getWifiConfig(wifiEntry, null /* scanResult */);
}
mUserSelectionCallback.select(config);
}
@Override
public void onCancel(@NonNull DialogInterface dialog) {
super.onCancel(dialog);
if (mUserSelectionCallback != null) {
mUserSelectionCallback.reject();
}
}
@Override
public void onDestroy() {
mWorkerThread.quit();
super.onDestroy();
}
private void showAllButton() {
final AlertDialog alertDialog = (AlertDialog) getDialog();
if (alertDialog == null) {
return;
}
final Button neutralBtn = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL);
if (neutralBtn != null) {
neutralBtn.setVisibility(View.VISIBLE);
}
}
private void hideProgressIcon() {
final AlertDialog alertDialog = (AlertDialog) getDialog();
if (alertDialog == null) {
return;
}
final View progress = alertDialog.findViewById(R.id.network_request_title_progress);
if (progress != null) {
progress.setVisibility(View.GONE);
}
}
/** Called when the state of Wifi has changed. */
@Override
public void onWifiStateChanged() {
if (mMatchWifis.size() == 0) {
return;
}
updateWifiEntries();
updateUi();
}
/**
* Update the results when data changes
*/
@Override
public void onWifiEntriesChanged() {
if (mMatchWifis.size() == 0) {
return;
}
updateWifiEntries();
updateUi();
}
@Override
public void onNumSavedSubscriptionsChanged() {
// Do nothing.
}
@Override
public void onNumSavedNetworksChanged() {
// Do nothing.
}
@VisibleForTesting
void updateWifiEntries() {
final List<WifiEntry> wifiEntries = new ArrayList<>();
WifiEntry connectedWifiEntry = mWifiPickerTracker.getConnectedWifiEntry();
String connectedSsid;
if (connectedWifiEntry != null) {
connectedSsid = connectedWifiEntry.getSsid();
wifiEntries.add(connectedWifiEntry);
} else {
connectedSsid = null;
}
wifiEntries.addAll(mWifiPickerTracker.getWifiEntries());
mFilteredWifiEntries.clear();
mFilteredWifiEntries.addAll(wifiEntries.stream()
.filter(entry -> isMatchedWifiEntry(entry, connectedSsid))
.limit(mShowLimitedItem ? MAX_NUMBER_LIST_ITEM : Long.MAX_VALUE)
.toList());
}
private boolean isMatchedWifiEntry(WifiEntry entry, String connectedSsid) {
if (entry.getConnectedState() == WifiEntry.CONNECTED_STATE_DISCONNECTED
&& TextUtils.equals(entry.getSsid(), connectedSsid)) {
// WifiPickerTracker may return a duplicate unsaved network that is separate from
// the connecting app-requested network, so make sure we only show the connected
// app-requested one.
return false;
}
for (MatchWifi wifi : mMatchWifis) {
if (!TextUtils.equals(entry.getSsid(), wifi.mSsid)) {
continue;
}
for (Integer security : wifi.mSecurityTypes) {
if (entry.getSecurityTypes().contains(security)) {
return true;
}
}
}
return false;
}
private class WifiEntryAdapter extends ArrayAdapter<WifiEntry> {
private final int mResourceId;
private final LayoutInflater mInflater;
WifiEntryAdapter(Context context, int resourceId, List<WifiEntry> objects) {
super(context, resourceId, objects);
mResourceId = resourceId;
mInflater = LayoutInflater.from(context);
}
@Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
view = mInflater.inflate(mResourceId, parent, false);
final View divider = view.findViewById(
com.android.settingslib.R.id.two_target_divider);
divider.setVisibility(View.GONE);
}
final WifiEntry wifiEntry = getItem(position);
final TextView titleView = view.findViewById(android.R.id.title);
if (titleView != null) {
// Shows whole SSID for better UX.
titleView.setSingleLine(false);
titleView.setText(wifiEntry.getTitle());
}
final TextView summary = view.findViewById(android.R.id.summary);
if (summary != null) {
final String summaryString = wifiEntry.getSummary();
if (TextUtils.isEmpty(summaryString)) {
summary.setVisibility(View.GONE);
} else {
summary.setVisibility(View.VISIBLE);
summary.setText(summaryString);
}
}
final PreferenceImageView imageView = view.findViewById(android.R.id.icon);
final int level = wifiEntry.getLevel();
if (imageView != null && level != WifiEntry.WIFI_LEVEL_UNREACHABLE) {
final Drawable drawable = getContext().getDrawable(
Utils.getWifiIconResource(level));
drawable.setTintList(
Utils.getColorAttr(getContext(), android.R.attr.colorControlNormal));
imageView.setImageDrawable(drawable);
}
return view;
}
}
@Override
public void onUserSelectionCallbackRegistration(
NetworkRequestUserSelectionCallback userSelectionCallback) {
mUserSelectionCallback = userSelectionCallback;
}
@Override
public void onMatch(List<ScanResult> scanResults) {
mMatchWifis.clear();
for (ScanResult scanResult : scanResults) {
MatchWifi matchWifi = new MatchWifi();
matchWifi.mSsid = scanResult.SSID;
matchWifi.mSecurityTypes = getSecurityTypesFromScanResult(scanResult);
mMatchWifis.add(matchWifi);
}
updateWifiEntries();
updateUi();
}
@VisibleForTesting
void updateUi() {
// Update related UI buttons
if (mShowLimitedItem && mFilteredWifiEntries.size() >= MAX_NUMBER_LIST_ITEM) {
showAllButton();
}
if (mFilteredWifiEntries.size() > 0) {
hideProgressIcon();
}
if (getDialogAdapter() != null) {
getDialogAdapter().notifyDataSetChanged();
}
}
}