Merge "Provide a way to forget networks which are saved with wrong password" into pi-car-dev
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);
+    }
+}