Provide a way to forget networks which are saved with wrong password
Bug: 63595267
Test: manual, robolectric
Change-Id: Ib18f412aa595fd9f81d2600c01492764480b64ec
diff --git a/res/drawable/ic_delete.xml b/res/drawable/ic_delete.xml
new file mode 100644
index 0000000..1f358ca
--- /dev/null
+++ b/res/drawable/ic_delete.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.
+-->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/icon_size"
+ android:height="@dimen/icon_size"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="?attr/iconColor"
+ android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
+</vector>
diff --git a/res/layout/delete_preference_widget.xml b/res/layout/delete_preference_widget.xml
new file mode 100644
index 0000000..7c2690e
--- /dev/null
+++ b/res/layout/delete_preference_widget.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.
+-->
+
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:src="@drawable/ic_delete"/>
diff --git a/src/com/android/car/settings/wifi/AccessPointListPreferenceController.java b/src/com/android/car/settings/wifi/AccessPointListPreferenceController.java
index 10c7f84..f53c41b 100644
--- a/src/com/android/car/settings/wifi/AccessPointListPreferenceController.java
+++ b/src/com/android/car/settings/wifi/AccessPointListPreferenceController.java
@@ -20,7 +20,6 @@
import android.car.drivingstate.CarUxRestrictionsManager;
import android.content.Context;
import android.net.wifi.WifiManager;
-import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
@@ -37,7 +36,7 @@
import java.util.List;
/**
- * Renders a list of {@link AccessPoint} as a list of preference.
+ * Renders a list of {@link AccessPoint} as a list of preferences.
*/
public class AccessPointListPreferenceController extends
WifiBasePreferenceController<PreferenceGroup> implements
@@ -45,21 +44,9 @@
Preference.OnPreferenceChangeListener,
CarUxRestrictionsManager.OnUxRestrictionsChangedListener {
private static final Logger LOG = new Logger(AccessPointListPreferenceController.class);
- private List<AccessPoint> mAccessPoints = new ArrayList<>();
-
private final WifiManager.ActionListener mConnectionListener =
- new WifiManager.ActionListener() {
- @Override
- public void onSuccess() {
- }
-
- @Override
- public void onFailure(int reason) {
- Toast.makeText(getContext(),
- R.string.wifi_failed_connect_message,
- Toast.LENGTH_SHORT).show();
- }
- };
+ new WifiUtil.ActionFailedListener(getContext(), R.string.wifi_failed_connect_message);
+ private List<AccessPoint> mAccessPoints = new ArrayList<>();
public AccessPointListPreferenceController(@NonNull Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
@@ -115,7 +102,8 @@
getCarWifiManager().connectToPublicWifi(accessPoint, mConnectionListener);
} else if (accessPoint.isActive()) {
getFragmentController().launchFragment(WifiDetailsFragment.getInstance(accessPoint));
- } else if (accessPoint.isSaved()) {
+ } else if (accessPoint.isSaved() && !WifiUtil.isAccessPointDisabledByWrongPassword(
+ accessPoint)) {
getCarWifiManager().connectToSavedWifi(accessPoint, mConnectionListener);
}
return true;
@@ -139,6 +127,15 @@
accessPointPreference.setSummary(accessPoint.getSummary());
accessPointPreference.setOnPreferenceClickListener(this);
accessPointPreference.setOnPreferenceChangeListener(this);
+ accessPointPreference.showButton(false);
+
+ if (accessPoint.isSaved() && WifiUtil.isAccessPointDisabledByWrongPassword(accessPoint)) {
+ accessPointPreference.setWidgetLayoutResource(R.layout.delete_preference_widget);
+ accessPointPreference.setOnButtonClickListener(
+ preference -> WifiUtil.forget(getContext(), accessPoint));
+ accessPointPreference.showButton(true);
+ }
+
return accessPointPreference;
}
}
diff --git a/src/com/android/car/settings/wifi/AccessPointPreference.java b/src/com/android/car/settings/wifi/AccessPointPreference.java
index cddafee..f0e2a8d 100644
--- a/src/com/android/car/settings/wifi/AccessPointPreference.java
+++ b/src/com/android/car/settings/wifi/AccessPointPreference.java
@@ -19,16 +19,14 @@
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
-import android.net.wifi.WifiConfiguration;
import androidx.preference.PreferenceViewHolder;
import com.android.car.settings.common.Logger;
-import com.android.car.settings.common.PasswordEditTextPreference;
import com.android.settingslib.wifi.AccessPoint;
/** Renders a {@link AccessPoint} as a preference. */
-public class AccessPointPreference extends PasswordEditTextPreference {
+public class AccessPointPreference extends ButtonPasswordEditTextPreference {
private static final Logger LOG = new Logger(AccessPointPreference.class);
private static final int[] STATE_SECURED = {
com.android.settingslib.R.attr.state_encrypted
@@ -77,7 +75,7 @@
*/
private boolean shouldShowPasswordDialog() {
return mAccessPoint.getSecurity() != AccessPoint.SECURITY_NONE && (!mAccessPoint.isSaved()
- || isAccessPointDisabledByWrongPassword(mAccessPoint));
+ || WifiUtil.isAccessPointDisabledByWrongPassword(mAccessPoint));
}
private Drawable getAccessPointIcon() {
@@ -93,18 +91,4 @@
drawable.setLevel(mAccessPoint.getLevel());
return drawable;
}
-
- private boolean isAccessPointDisabledByWrongPassword(AccessPoint accessPoint) {
- WifiConfiguration config = accessPoint.getConfig();
- if (config == null) {
- return false;
- }
- WifiConfiguration.NetworkSelectionStatus networkStatus =
- config.getNetworkSelectionStatus();
- if (networkStatus == null || networkStatus.isNetworkEnabled()) {
- return false;
- }
- return networkStatus.getNetworkSelectionDisableReason()
- == WifiConfiguration.NetworkSelectionStatus.DISABLED_BY_WRONG_PASSWORD;
- }
}
diff --git a/src/com/android/car/settings/wifi/ButtonPasswordEditTextPreference.java b/src/com/android/car/settings/wifi/ButtonPasswordEditTextPreference.java
new file mode 100644
index 0000000..46834c1
--- /dev/null
+++ b/src/com/android/car/settings/wifi/ButtonPasswordEditTextPreference.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2019 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.car.settings.wifi;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.PasswordEditTextPreference;
+
+/**
+ * A {@link PasswordEditTextPreference} which has a second button which can perform another action
+ * defined by {@link OnButtonClickListener}.
+ */
+public class ButtonPasswordEditTextPreference extends PasswordEditTextPreference {
+
+ private OnButtonClickListener mOnButtonClickListener;
+
+ private boolean mIsButtonShown = true;
+
+ public ButtonPasswordEditTextPreference(Context context) {
+ super(context);
+ init();
+ }
+
+ private void init() {
+ setLayoutResource(R.layout.two_action_preference);
+ }
+
+ /**
+ * Sets whether the secondary button is visible in the preference.
+ *
+ * @param isShown {@code true} if the secondary button should be shown.
+ */
+ public void showButton(boolean isShown) {
+ mIsButtonShown = isShown;
+ notifyChanged();
+ }
+
+ /** Returns {@code true} if action is shown. */
+ public boolean isButtonShown() {
+ return mIsButtonShown;
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+ View actionConatiner = holder.findViewById(R.id.action_widget_container);
+ View widgetFrame = holder.findViewById(android.R.id.widget_frame);
+ if (mIsButtonShown) {
+ actionConatiner.setVisibility(View.VISIBLE);
+ widgetFrame.setOnClickListener(v -> performButtonClick());
+ } else {
+ actionConatiner.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Sets an {@link OnButtonClickListener} to be invoked when the button is clicked.
+ */
+ public void setOnButtonClickListener(OnButtonClickListener listener) {
+ mOnButtonClickListener = listener;
+ }
+
+ /** Virtually clicks the button contained inside this preference. */
+ public void performButtonClick() {
+ if (isButtonShown()) {
+ if (mOnButtonClickListener != null) {
+ mOnButtonClickListener.onButtonClick(this);
+ }
+ }
+ }
+
+ /** Callback to be invoked when the button is clicked. */
+ public interface OnButtonClickListener {
+ /**
+ * Called when a button has been clicked.
+ *
+ * @param preference the preference whose button was clicked.
+ */
+ void onButtonClick(ButtonPasswordEditTextPreference preference);
+ }
+}
diff --git a/src/com/android/car/settings/wifi/WifiUtil.java b/src/com/android/car/settings/wifi/WifiUtil.java
index 15f0134..6a33854 100644
--- a/src/com/android/car/settings/wifi/WifiUtil.java
+++ b/src/com/android/car/settings/wifi/WifiUtil.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.provider.Settings;
@@ -30,6 +31,7 @@
import androidx.annotation.StringRes;
import com.android.car.settings.R;
+import com.android.car.settings.common.Logger;
import com.android.settingslib.wifi.AccessPoint;
import java.util.regex.Pattern;
@@ -39,6 +41,8 @@
*/
public class WifiUtil {
+ private static final Logger LOG = new Logger(WifiUtil.class);
+
/** Value that is returned when we fail to connect wifi. */
public static final int INVALID_NET_ID = -1;
private static final Pattern HEX_PATTERN = Pattern.compile("^[0-9A-F]+$");
@@ -201,7 +205,66 @@
return netId;
}
+ /** Forget the network specified by {@code accessPoint}. */
+ public static void forget(Context context, AccessPoint accessPoint) {
+ WifiManager wifiManager = context.getSystemService(WifiManager.class);
+ if (!accessPoint.isSaved()) {
+ if (accessPoint.getNetworkInfo() != null
+ && accessPoint.getNetworkInfo().getState() != NetworkInfo.State.DISCONNECTED) {
+ // Network is active but has no network ID - must be ephemeral.
+ wifiManager.disableEphemeralNetwork(
+ AccessPoint.convertToQuotedString(accessPoint.getSsidStr()));
+ } else {
+ // Should not happen, but a monkey seems to trigger it
+ LOG.e("Failed to forget invalid network " + accessPoint.getConfig());
+ return;
+ }
+ } else {
+ wifiManager.forget(accessPoint.getConfig().networkId,
+ new ActionFailedListener(context, R.string.wifi_failed_forget_message));
+ }
+ }
+
+ /** Returns {@code true} if the access point was disabled due to the wrong password. */
+ public static boolean isAccessPointDisabledByWrongPassword(AccessPoint accessPoint) {
+ WifiConfiguration config = accessPoint.getConfig();
+ if (config == null) {
+ return false;
+ }
+ WifiConfiguration.NetworkSelectionStatus networkStatus =
+ config.getNetworkSelectionStatus();
+ if (networkStatus == null || networkStatus.isNetworkEnabled()) {
+ return false;
+ }
+ return networkStatus.getNetworkSelectionDisableReason()
+ == WifiConfiguration.NetworkSelectionStatus.DISABLED_BY_WRONG_PASSWORD;
+ }
+
private static boolean isHexString(String password) {
return HEX_PATTERN.matcher(password).matches();
}
+
+ /**
+ * A shared implementation of {@link WifiManager.ActionListener} which shows a failure message
+ * in a toast.
+ */
+ public static class ActionFailedListener implements WifiManager.ActionListener {
+ private final Context mContext;
+ @StringRes
+ private final int mFailureMessage;
+
+ public ActionFailedListener(Context context, @StringRes int failureMessage) {
+ mContext = context;
+ mFailureMessage = failureMessage;
+ }
+
+ @Override
+ public void onSuccess() {
+ }
+
+ @Override
+ public void onFailure(int reason) {
+ Toast.makeText(mContext, mFailureMessage, Toast.LENGTH_SHORT).show();
+ }
+ }
}
diff --git a/src/com/android/car/settings/wifi/details/WifiDetailsFragment.java b/src/com/android/car/settings/wifi/details/WifiDetailsFragment.java
index 0e49c38..9027df8 100644
--- a/src/com/android/car/settings/wifi/details/WifiDetailsFragment.java
+++ b/src/com/android/car/settings/wifi/details/WifiDetailsFragment.java
@@ -21,7 +21,6 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
-import android.net.NetworkInfo.State;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
@@ -157,7 +156,7 @@
mForgetButton = getActivity().findViewById(R.id.action_button1);
mForgetButton.setText(R.string.forget);
mForgetButton.setOnClickListener(v -> {
- forget();
+ WifiUtil.forget(getContext(), mAccessPoint);
goBack();
});
}
@@ -243,22 +242,4 @@
LOG.d("wifiConfig is: " + wifiConfig);
return wifiConfig != null && !WifiUtil.isNetworkLockedDown(getContext(), wifiConfig);
}
-
- private void forget() {
- if (!mAccessPoint.isSaved()) {
- if (mAccessPoint.getNetworkInfo() != null
- && mAccessPoint.getNetworkInfo().getState() != State.DISCONNECTED) {
- // Network is active but has no network ID - must be ephemeral.
- mWifiManager.disableEphemeralNetwork(
- AccessPoint.convertToQuotedString(mAccessPoint.getSsidStr()));
- } else {
- // Should not happen, but a monkey seems to trigger it
- LOG.e("Failed to forget invalid network " + mAccessPoint.getConfig());
- return;
- }
- } else {
- mWifiManager.forget(mAccessPoint.getConfig().networkId,
- new ActionFailListener(R.string.wifi_failed_forget_message));
- }
- }
}
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowWifiManager.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowWifiManager.java
index 43c0710..77a8703 100644
--- a/tests/robotests/src/com/android/car/settings/testutils/ShadowWifiManager.java
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowWifiManager.java
@@ -34,6 +34,7 @@
private final Map<Integer, WifiConfiguration> mNetworkIdToConfiguredNetworks =
new LinkedHashMap<>();
+ private int mLastForgottenNetwork = Integer.MIN_VALUE;
@Implementation
@Override
@@ -57,6 +58,15 @@
}
@Implementation
+ protected void forget(int netId, WifiManager.ActionListener listener) {
+ mLastForgottenNetwork = netId;
+ }
+
+ public int getLastForgottenNetwork() {
+ return mLastForgottenNetwork;
+ }
+
+ @Implementation
protected void factoryReset() {
sResetCalledCount++;
}
diff --git a/tests/robotests/src/com/android/car/settings/wifi/AccessPointListPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/wifi/AccessPointListPreferenceControllerTest.java
index f8cc53a..7ecf4ae 100644
--- a/tests/robotests/src/com/android/car/settings/wifi/AccessPointListPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/car/settings/wifi/AccessPointListPreferenceControllerTest.java
@@ -20,6 +20,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -179,6 +180,33 @@
}
@Test
+ public void performButtonClick_savedAccessPoint_wrongPassword_forgetsNetwork() {
+ int netId = 1;
+
+ WifiConfiguration config = mock(WifiConfiguration.class);
+ WifiConfiguration.NetworkSelectionStatus status = mock(
+ WifiConfiguration.NetworkSelectionStatus.class);
+ config.networkId = netId;
+ when(mMockAccessPoint1.getSecurity()).thenReturn(AccessPoint.SECURITY_PSK);
+ when(mMockAccessPoint1.isSaved()).thenReturn(true);
+ when(mMockAccessPoint1.getConfig()).thenReturn(config);
+ when(config.getNetworkSelectionStatus()).thenReturn(status);
+ when(status.isNetworkEnabled()).thenReturn(false);
+ when(status.getNetworkSelectionDisableReason()).thenReturn(
+ WifiConfiguration.NetworkSelectionStatus.DISABLED_BY_WRONG_PASSWORD);
+
+ List<AccessPoint> accessPointList = Arrays.asList(mMockAccessPoint1);
+ when(mMockCarWifiManager.getAllAccessPoints()).thenReturn(accessPointList);
+ mController.refreshUi();
+
+ ButtonPasswordEditTextPreference preference =
+ (ButtonPasswordEditTextPreference) mPreferenceGroup.getPreference(0);
+ preference.performButtonClick();
+
+ assertThat(getShadowWifiManager().getLastForgottenNetwork()).isEqualTo(netId);
+ }
+
+ @Test
public void callChangeListener_newSecureAccessPoint_wifiAdded() {
String ssid = "test_ssid";
String password = "test_password";
diff --git a/tests/robotests/src/com/android/car/settings/wifi/ButtonPasswordEditTextPreferenceTest.java b/tests/robotests/src/com/android/car/settings/wifi/ButtonPasswordEditTextPreferenceTest.java
new file mode 100644
index 0000000..4ed3a57
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/wifi/ButtonPasswordEditTextPreferenceTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2019 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.car.settings.wifi;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.view.View;
+
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(CarSettingsRobolectricTestRunner.class)
+public class ButtonPasswordEditTextPreferenceTest {
+
+ private PreferenceViewHolder mViewHolder;
+ private ButtonPasswordEditTextPreference mButtonPreference;
+
+ @Before
+ public void setUp() {
+ View rootView = View.inflate(RuntimeEnvironment.application, R.layout.two_action_preference,
+ null);
+ mViewHolder = PreferenceViewHolder.createInstanceForTests(rootView);
+ mButtonPreference = new ButtonPasswordEditTextPreference(RuntimeEnvironment.application);
+ }
+
+ @Test
+ public void buttonClicked_callsListener() {
+ mButtonPreference.onBindViewHolder(mViewHolder);
+ ButtonPasswordEditTextPreference.OnButtonClickListener listener = mock(
+ ButtonPasswordEditTextPreference.OnButtonClickListener.class);
+ mButtonPreference.setOnButtonClickListener(listener);
+
+ mViewHolder.findViewById(android.R.id.widget_frame).performClick();
+
+ verify(listener).onButtonClick(mButtonPreference);
+ }
+
+ @Test
+ public void performButtonClick_listenerSetAndButtonVisible_listenerFired() {
+ ButtonPasswordEditTextPreference.OnButtonClickListener listener = mock(
+ ButtonPasswordEditTextPreference.OnButtonClickListener.class);
+ mButtonPreference.setOnButtonClickListener(listener);
+ mButtonPreference.showButton(true);
+
+ mButtonPreference.performButtonClick();
+ verify(listener).onButtonClick(mButtonPreference);
+ }
+
+ @Test
+ public void performButtonClick_listenerSetAndButtonInvisible_listenerNotFired() {
+ ButtonPasswordEditTextPreference.OnButtonClickListener listener = mock(
+ ButtonPasswordEditTextPreference.OnButtonClickListener.class);
+ mButtonPreference.setOnButtonClickListener(listener);
+ mButtonPreference.showButton(false);
+
+ mButtonPreference.performButtonClick();
+ verify(listener, never()).onButtonClick(mButtonPreference);
+ }
+}