Reland "[AW][Dev-UI] Reland "Add some espresso tests for flag UI""

This is a reland of db9be6c9d1c4f61897dd047d9fa1f7821b244e15

The recent flakes are most probably caused by using of UIAutomator APIs
to test the developer UI service notification and pressing home button
in the tearDown of tests. This reland the CL after removing all
UIAutomator code including the test for persistent notification. A
separate reland of that test would be done later.

Original change's description:
> [AW][Dev-UI] Reland "Add some espresso tests for flag UI"
>
> This relands https://crbug.com/c/2438534 with a couple small changes:
>
> - android:windowSoftInputMode="adjustPan" attribute was missing in the
>   test manifest but is used in prod manifest. Adding it to the tests
>   manifest fixes the problem of auto-showing the keyboard on M devices
>   which was causing the test flakiness. When the keyboard shows up it
>   hides the view causing espresso to fail.
>
> - Merge the small change from https://crrev.com/c/2455687 to
>   test whether the overrideFlagsMap has the correct entries.
>
> Fixed: 1135839, 1130599
> Test: run_webview_instrumentation_test_apk -f "*FlagsFragmentTest*" on android M emulator
> Change-Id: I973e8cd2b7a9c12651d7a6e14f11a368f6e06bad
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2453564
> Reviewed-by: Changwan Ryu <changwan@chromium.org>
> Reviewed-by: Nate Fischer <ntfschr@chromium.org>
> Commit-Queue: Hazem Ashmawy <hazems@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#817184}

Change-Id: I9089d7e8d9b61b46b412061715abbf981da03706
Bug: 1130599, 1135839
Test: run_webview_instrumentation_test_apk -f "*FlagsFragmentTest*" --num_retries=0 --repeat=1000 --break-on-failure
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2482142
Reviewed-by: Nate Fischer <ntfschr@chromium.org>
Reviewed-by: Changwan Ryu <changwan@chromium.org>
Commit-Queue: Hazem Ashmawy <hazems@chromium.org>
Cr-Commit-Position: refs/heads/master@{#818858}
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/devui/DeveloperUiTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/devui/DeveloperUiTest.java
index 30db823..081151c 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/devui/DeveloperUiTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/devui/DeveloperUiTest.java
@@ -57,7 +57,8 @@
 public class DeveloperUiTest {
     // The package name of the test shell. This is acting both as the client app and the WebView
     // provider.
-    private static final String TEST_WEBVIEW_PACKAGE_NAME = "org.chromium.android_webview.shell";
+    public static final String TEST_WEBVIEW_PACKAGE_NAME = "org.chromium.android_webview.shell";
+    public static final String TEST_WEBVIEW_APPLICATION_LABEL = "AwShellApplication";
 
     @Rule
     public IntentsTestRule mRule = new IntentsTestRule<MainActivity>(MainActivity.class);
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/devui/FlagsFragmentTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/devui/FlagsFragmentTest.java
index bcb200e..cca9c80 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/devui/FlagsFragmentTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/devui/FlagsFragmentTest.java
@@ -4,24 +4,33 @@
 
 package org.chromium.android_webview.test.devui;
 
+import static androidx.test.espresso.Espresso.onData;
 import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
 import static androidx.test.espresso.action.ViewActions.replaceText;
 import static androidx.test.espresso.assertion.ViewAssertions.matches;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withSpinnerText;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.anything;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.lessThan;
 import static org.hamcrest.Matchers.not;
+import static org.hamcrest.collection.IsMapContaining.hasEntry;
 
 import static org.chromium.android_webview.test.devui.DeveloperUiTestUtils.withCount;
 
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
 import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.rule.ActivityTestRule;
 import android.view.MotionEvent;
 import android.view.View;
@@ -30,31 +39,40 @@
 import android.widget.TextView;
 
 import androidx.annotation.IntDef;
+import androidx.test.espresso.DataInteraction;
 import androidx.test.filters.MediumTest;
 
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.TypeSafeMatcher;
+import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import org.chromium.android_webview.common.AwSwitches;
+import org.chromium.android_webview.common.DeveloperModeUtils;
+import org.chromium.android_webview.common.Flag;
 import org.chromium.android_webview.devui.FlagsFragment;
 import org.chromium.android_webview.devui.MainActivity;
 import org.chromium.android_webview.devui.R;
+import org.chromium.android_webview.services.DeveloperUiService;
 import org.chromium.android_webview.test.AwJUnit4ClassRunner;
-import org.chromium.base.test.util.Batch;
 import org.chromium.base.test.util.CallbackHelper;
 import org.chromium.base.test.util.Feature;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
 
+import java.util.Map;
+
 /**
  * UI tests for {@link FlagsFragment}.
+ * <p>
+ * These tests should not be batched to make sure that the DeveloperUiService is killed
+ * after each test, leaving a clean state.
  */
 @RunWith(AwJUnit4ClassRunner.class)
-@Batch(Batch.PER_CLASS)
 public class FlagsFragmentTest {
     @Rule
     public ActivityTestRule mRule =
@@ -67,6 +85,12 @@
         mRule.launchActivity(intent);
     }
 
+    @After
+    public void tearDown() {
+        // Make sure to clear shared preferences between tests to avoid any saved state.
+        DeveloperUiService.clearSharedPrefsForTesting(InstrumentationRegistry.getTargetContext());
+    }
+
     private CallbackHelper getFlagUiSearchBarListener() {
         final CallbackHelper helper = new CallbackHelper();
         FlagsFragment.setFilterListener(() -> { helper.notifyCalled(); });
@@ -318,4 +342,121 @@
         onView(withId(R.id.flag_search_bar))
                 .check(matches(compoundDrawableVisible(CompoundDrawable.END)));
     }
+
+    /**
+     * Toggle a flag's spinner with the given state value, and check that text changes to the
+     * correct value.
+     *
+     * @param flagInteraction the {@link DataInteraction} object representing a flag View item via
+     *         {@code onData()}.
+     * @param state {@code true} for "enabled", {@code false} for "disabled", {@code null} for
+     *         Default.
+     * @return the same {@code flagInteraction} passed param for the ease of chaining.
+     */
+    private DataInteraction toggleFlag(DataInteraction flagInteraction, Boolean state) {
+        String stateText = state == null ? "Default" : state ? "Enabled" : "Disabled";
+        flagInteraction.onChildView(withId(R.id.flag_toggle)).perform(click());
+        onData(allOf(is(instanceOf(String.class)), is(stateText))).perform(click());
+        flagInteraction.onChildView(withId(R.id.flag_toggle))
+                .check(matches(withSpinnerText(containsString(stateText))));
+
+        return flagInteraction;
+    }
+
+    @Test
+    @MediumTest
+    @Feature({"AndroidWebView"})
+    public void testTogglingFlagShowsBlueDot() throws Throwable {
+        DataInteraction flagInteraction =
+                onData(anything()).inAdapterView(withId(R.id.flags_list)).atPosition(1);
+
+        // blue dot should be hidden by default
+        flagInteraction.onChildView(withId(R.id.flag_name))
+                .check(matches(not(compoundDrawableVisible(CompoundDrawable.START))));
+
+        // Test enabling flags shows a bluedot next to flag name
+        toggleFlag(flagInteraction, true);
+        flagInteraction.onChildView(withId(R.id.flag_name))
+                .check(matches(compoundDrawableVisible(CompoundDrawable.START)));
+
+        // Test setting to default hide the blue dot
+        toggleFlag(flagInteraction, null);
+        flagInteraction.onChildView(withId(R.id.flag_name))
+                .check(matches(not(compoundDrawableVisible(CompoundDrawable.START))));
+
+        // Test disabling flags shows a bluedot next to flag name
+        toggleFlag(flagInteraction, false);
+        flagInteraction.onChildView(withId(R.id.flag_name))
+                .check(matches(compoundDrawableVisible(CompoundDrawable.START)));
+
+        // Test setting to default again hide the blue dot
+        toggleFlag(flagInteraction, null);
+        flagInteraction.onChildView(withId(R.id.flag_name))
+                .check(matches(not(compoundDrawableVisible(CompoundDrawable.START))));
+    }
+
+    @Test
+    @MediumTest
+    @Feature({"AndroidWebView"})
+    public void testToggledFlagsFloatToTop() throws Throwable {
+        ListView flagsList = mRule.getActivity().findViewById(R.id.flags_list);
+        int totalNumFlags = flagsList.getCount();
+        String lastFlagName = ((Flag) flagsList.getAdapter().getItem(totalNumFlags - 1)).getName();
+
+        // Toggle the last flag in the list.
+        toggleFlag(onData(anything())
+                           .inAdapterView(withId(R.id.flags_list))
+                           .atPosition(totalNumFlags - 1),
+                true);
+        // Navigate from the flags UI then back to it to trigger list sorting.
+        onView(withId(R.id.navigation_home)).perform(click());
+        onView(withId(R.id.navigation_flags_ui)).perform(click());
+
+        // Check that the toggled flag is now at the top of the list. This assumes that the flags
+        // list has > 2 items.
+        onData(anything())
+                .inAdapterView(withId(R.id.flags_list))
+                .atPosition(1)
+                .onChildView(withId(R.id.flag_name))
+                .check(matches(withText(lastFlagName)));
+
+        // Reset to default.
+        toggleFlag(onData(anything()).inAdapterView(withId(R.id.flags_list)).atPosition(1), null);
+        // Navigate from the flags UI then back to it to trigger list sorting.
+        onView(withId(R.id.navigation_home)).perform(click());
+        onView(withId(R.id.navigation_flags_ui)).perform(click());
+        // Check that flags goes back to the end of the list when untoggled.
+        onData(anything())
+                .inAdapterView(withId(R.id.flags_list))
+                .atPosition(totalNumFlags - 1)
+                .onChildView(withId(R.id.flag_name))
+                .check(matches(withText(lastFlagName)));
+    }
+
+    @Test
+    @MediumTest
+    @Feature({"AndroidWebView"})
+    public void testResetFlags() throws Throwable {
+        ListView flagsList = mRule.getActivity().findViewById(R.id.flags_list);
+        String firstFlagName = ((Flag) flagsList.getAdapter().getItem(1)).getName();
+
+        toggleFlag(onData(anything()).inAdapterView(withId(R.id.flags_list)).atPosition(1), true);
+        Map<String, Boolean> flagOverrides =
+                DeveloperModeUtils.getFlagOverrides(DeveloperUiTest.TEST_WEBVIEW_PACKAGE_NAME);
+        assertThat(
+                "flagOverrides map should contain exactly one entry", flagOverrides.size(), is(1));
+        assertThat(flagOverrides, hasEntry(firstFlagName, true));
+
+        onView(withId(R.id.reset_flags_button)).perform(click());
+
+        DataInteraction flagInteraction =
+                onData(anything()).inAdapterView(withId(R.id.flags_list)).atPosition(1);
+        flagInteraction.onChildView(withId(R.id.flag_name))
+                .check(matches(not(compoundDrawableVisible(CompoundDrawable.START))));
+        flagInteraction.onChildView(withId(R.id.flag_toggle))
+                .check(matches(withSpinnerText(containsString("Default"))));
+        Assert.assertTrue(
+                DeveloperModeUtils.getFlagOverrides(DeveloperUiTest.TEST_WEBVIEW_PACKAGE_NAME)
+                        .isEmpty());
+    }
 }
diff --git a/android_webview/nonembedded/java/AndroidManifest.xml b/android_webview/nonembedded/java/AndroidManifest.xml
index 41d136e..c0e84fa 100644
--- a/android_webview/nonembedded/java/AndroidManifest.xml
+++ b/android_webview/nonembedded/java/AndroidManifest.xml
@@ -54,6 +54,8 @@
                 </intent-filter>
             </activity>
             <!-- Don't actually try to launch with this alias: it only exists so we can query its enabled state. -->
+            <!-- If you change this component make sure to update the corresponding copy in
+                 test/shell/AndroidManifest.xml -->
             <activity-alias android:name="org.chromium.android_webview.devui.DeveloperModeState"
                       android:targetActivity="org.chromium.android_webview.devui.MainActivity"
                       android:visibleToInstantApps="true"
@@ -79,6 +81,8 @@
                       android:process=":webview_apk"  {# Explicit process required for monochrome compatibility. #}
                       tools:ignore="ExportedContentProvider"/>
             <!-- Disabled by default, enabled at runtime by Developer UI. -->
+            <!-- If you change this component make sure to update the corresponding copy in
+                 test/shell/AndroidManifest.xml-->
             <provider android:name="org.chromium.android_webview.services.DeveloperModeContentProvider"
                       android:visibleToInstantApps="true"
                       android:exported="true"
diff --git a/android_webview/nonembedded/java/src/org/chromium/android_webview/devui/FlagsFragment.java b/android_webview/nonembedded/java/src/org/chromium/android_webview/devui/FlagsFragment.java
index fff28f9..2a1eb6cab 100644
--- a/android_webview/nonembedded/java/src/org/chromium/android_webview/devui/FlagsFragment.java
+++ b/android_webview/nonembedded/java/src/org/chromium/android_webview/devui/FlagsFragment.java
@@ -433,10 +433,11 @@
         TextView flagName = toggleableFlag.findViewById(R.id.flag_name);
         if (state == /* STATE_DEFAULT */ 0) {
             // Unset the compound drawable.
-            flagName.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+            flagName.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
         } else { // STATE_ENABLED or STATE_DISABLED
             // Draws a blue circle to the left of the text.
-            flagName.setCompoundDrawablesWithIntrinsicBounds(R.drawable.blue_circle, 0, 0, 0);
+            flagName.setCompoundDrawablesRelativeWithIntrinsicBounds(
+                    R.drawable.blue_circle, 0, 0, 0);
         }
     }
 
diff --git a/android_webview/nonembedded/java/src/org/chromium/android_webview/services/DeveloperUiService.java b/android_webview/nonembedded/java/src/org/chromium/android_webview/services/DeveloperUiService.java
index 8cb4f44d..4977626 100644
--- a/android_webview/nonembedded/java/src/org/chromium/android_webview/services/DeveloperUiService.java
+++ b/android_webview/nonembedded/java/src/org/chromium/android_webview/services/DeveloperUiService.java
@@ -21,6 +21,8 @@
 import android.os.Process;
 import android.os.RemoteException;
 
+import androidx.annotation.VisibleForTesting;
+
 import org.chromium.android_webview.common.DeveloperModeUtils;
 import org.chromium.android_webview.common.Flag;
 import org.chromium.android_webview.common.FlagOverrideHelper;
@@ -51,6 +53,11 @@
     private static final int FRAGMENT_ID_CRASHES = 1;
     private static final int FRAGMENT_ID_FLAGS = 2;
 
+    public static final String NOTIFICATION_TITLE =
+            "WARNING: experimental WebView features enabled";
+    public static final String NOTIFICATION_CONTENT = "Tap to see experimental features.";
+    public static final String NOTIFICATION_TICKER = "Experimental WebView features enabled";
+
     private static final Object sLock = new Object();
     @GuardedBy("sLock")
     private static Map<String, Boolean> sOverriddenFlags = new HashMap<>();
@@ -221,15 +228,14 @@
         notificationIntent.putExtra(FRAGMENT_ID_INTENT_EXTRA, FRAGMENT_ID_FLAGS);
         PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
 
-        Notification.Builder builder =
-                createNotificationBuilder()
-                        .setContentTitle("WARNING: experimental WebView features enabled")
-                        .setContentText("Tap to see experimental features.")
-                        .setSmallIcon(android.R.drawable.stat_notify_error)
-                        .setContentIntent(pendingIntent)
-                        .setOngoing(true)
-                        .setVisibility(Notification.VISIBILITY_PUBLIC)
-                        .setTicker("Experimental WebView features enabled");
+        Notification.Builder builder = createNotificationBuilder()
+                                               .setContentTitle(NOTIFICATION_TITLE)
+                                               .setContentText(NOTIFICATION_CONTENT)
+                                               .setSmallIcon(android.R.drawable.stat_notify_error)
+                                               .setContentIntent(pendingIntent)
+                                               .setOngoing(true)
+                                               .setVisibility(Notification.VISIBILITY_PUBLIC)
+                                               .setTicker(NOTIFICATION_TICKER);
 
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
             builder = builder
@@ -310,4 +316,14 @@
         FlagOverrideHelper helper = new FlagOverrideHelper(ProductionSupportedFlagList.sFlagList);
         helper.applyFlagOverrides(newFlags);
     }
+
+    @VisibleForTesting
+    public static void clearSharedPrefsForTesting(Context context) {
+        synchronized (sLock) {
+            context.getSharedPreferences(DeveloperUiService.SHARED_PREFS_FILE, Context.MODE_PRIVATE)
+                    .edit()
+                    .clear()
+                    .apply();
+        }
+    }
 }
diff --git a/android_webview/test/shell/AndroidManifest.xml b/android_webview/test/shell/AndroidManifest.xml
index 89f50a0..3aa6231 100644
--- a/android_webview/test/shell/AndroidManifest.xml
+++ b/android_webview/test/shell/AndroidManifest.xml
@@ -41,13 +41,6 @@
       </intent-filter>
     </activity>
 
-    <!-- WebView Developer UI Activities. If you change these Activities, please update
-         android_webview/nonembedded/java/AndroidManifest.xml as well. -->
-    <activity android:name="org.chromium.android_webview.devui.MainActivity"
-              android:theme="@style/Theme.DevUi.DayNight"
-              android:launchMode="singleTask">
-    </activity>
-
     <provider android:name="org.chromium.android_webview.test.TestContentProvider"
         android:authorities="org.chromium.android_webview.test.TestContentProvider" />
     <!-- Some tests require 3 sandboxed services -->
@@ -84,5 +77,21 @@
              android:exported="true" />
     <service android:name="org.chromium.android_webview.test.services.MockVariationsSeedServer"
              android:exported="true" />
+    <!-- Components for Developer UI, make sure that any changes in these components reflect
+         the corresponding original components in nonembedded/java/AndroidManifest.xml -->
+    <activity android:name="org.chromium.android_webview.devui.MainActivity"
+              android:theme="@style/Theme.DevUi.DayNight"
+              android:launchMode="singleTask"
+              android:windowSoftInputMode="adjustPan">
+    </activity>
+    <provider android:name="org.chromium.android_webview.services.DeveloperModeContentProvider"
+              android:exported="true"
+              android:authorities="org.chromium.android_webview.shell.DeveloperModeContentProvider"/>
+    <service android:name="org.chromium.android_webview.services.DeveloperUiService"
+                         android:exported="false" />
+    <activity-alias android:name="org.chromium.android_webview.devui.DeveloperModeState"
+                      android:targetActivity="org.chromium.android_webview.shell.AwShellActivity"
+                      android:enabled="false" />
+    <!-- End of Developer UI related components -->
   </application>
 </manifest>