Merge "TAPL: Verifying some interactions with system" into ub-launcher3-master
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 428e647..ab6393a 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -100,6 +100,7 @@
     @PortraitLandscape
     public void testOverview() throws Exception {
         startTestApps();
+        // mLauncher.pressHome() also tests an important case of pressing home while in background.
         Overview overview = mLauncher.pressHome().switchToOverview();
         assertTrue("Launcher internal state didn't switch to Overview",
                 isInState(LauncherState.OVERVIEW));
diff --git a/src/com/android/launcher3/testing/TestLogging.java b/src/com/android/launcher3/testing/TestLogging.java
new file mode 100644
index 0000000..024110d
--- /dev/null
+++ b/src/com/android/launcher3/testing/TestLogging.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 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.launcher3.testing;
+
+import android.util.Log;
+
+import com.android.launcher3.Utilities;
+
+public final class TestLogging {
+    public static synchronized void recordEvent(String event) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.TAPL_EVENTS_TAG, event);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 929315a..01c207f 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -31,6 +31,7 @@
     public static final int QUICK_SWITCH_STATE_ORDINAL = 4;
     public static final int ALL_APPS_STATE_ORDINAL = 5;
     public static final int BACKGROUND_APP_STATE_ORDINAL = 6;
+    public static final String TAPL_EVENTS_TAG = "TaplEvents";
 
     public static String stateOrdinalToString(int ordinal) {
         switch (ordinal) {
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index ba1bfa5..bee7853 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.folder.Folder;
+import com.android.launcher3.testing.TestLogging;
 
 /**
  * Class to handle long-clicks on workspace items and start drag as a result.
@@ -46,6 +47,7 @@
             ItemLongClickListener::onAllAppsItemLongClick;
 
     private static boolean onWorkspaceItemLongClick(View v) {
+        TestLogging.recordEvent("onWorkspaceItemLongClick");
         Launcher launcher = Launcher.getLauncher(v.getContext());
         if (!canStartDrag(launcher)) return false;
         if (!launcher.isInState(NORMAL) && !launcher.isInState(OVERVIEW)) return false;
@@ -75,6 +77,7 @@
     }
 
     private static boolean onAllAppsItemLongClick(View v) {
+        TestLogging.recordEvent("onAllAppsItemLongClick");
         Launcher launcher = Launcher.getLauncher(v.getContext());
         if (!canStartDrag(launcher)) return false;
         // When we have exited all apps or are in transition, disregard long clicks
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 66fdc94..61ce046 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -25,7 +25,6 @@
 
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.util.Log;
 import android.view.GestureDetector;
 import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
@@ -37,10 +36,9 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -168,6 +166,7 @@
 
     @Override
     public void onLongPress(MotionEvent event) {
+        TestLogging.recordEvent("Workspace.longPress");
         if (mLongPressState == STATE_REQUESTED) {
             if (canHandleLongPress()) {
                 mLongPressState = STATE_PENDING_PARENT_INFORM;
@@ -178,9 +177,6 @@
                 mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
                         Action.Direction.NONE, ContainerType.WORKSPACE,
                         mWorkspace.getCurrentPage());
-                if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-                    Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Opening options popup on long press");
-                }
                 OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
             } else {
                 cancelLongPress();
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 6cae43d..3758cb8 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -35,6 +35,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -90,6 +91,7 @@
 
     @Override
     public boolean onLongClick(View v) {
+        TestLogging.recordEvent("Widgets.onLongClick");
         if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
 
         if (v instanceof WidgetCell) {
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 2da6344..bf68f71 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -22,10 +22,15 @@
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.UiObject2;
 
+import java.util.regex.Pattern;
+
 /**
  * App icon, whether in all apps or in workspace/
  */
 public final class AppIcon extends Launchable {
+
+    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onAllAppsItemLongClick");
+
     AppIcon(LauncherInstrumentation launcher, UiObject2 icon) {
         super(launcher, icon);
     }
@@ -43,6 +48,11 @@
     }
 
     @Override
+    protected void addExpectedEventsForLongClick() {
+        mLauncher.expectEvent(LONG_CLICK_EVENT);
+    }
+
+    @Override
     protected String getLongPressIndicator() {
         return "deep_shortcuts_container";
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
index ba9c10e..597be90 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
@@ -34,6 +34,10 @@
     }
 
     @Override
+    protected void addExpectedEventsForLongClick() {
+    }
+
+    @Override
     protected String getLongPressIndicator() {
         return "drop_target_bar";
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 6881197..9327cb3 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -69,18 +69,24 @@
      * Drags an object to the center of homescreen.
      */
     public void dragToWorkspace() {
-        final Point launchableCenter = getObject().getVisibleCenter();
-        final Point displaySize = mLauncher.getRealDisplaySize();
-        final int width = displaySize.x / 2;
-        Workspace.dragIconToWorkspace(
-                mLauncher,
-                this,
-                new Point(
-                        launchableCenter.x >= width ?
-                                launchableCenter.x - width / 2 : launchableCenter.x + width / 2,
-                        displaySize.y / 2),
-                getLongPressIndicator());
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final Point launchableCenter = getObject().getVisibleCenter();
+            final Point displaySize = mLauncher.getRealDisplaySize();
+            final int width = displaySize.x / 2;
+            addExpectedEventsForLongClick();
+            Workspace.dragIconToWorkspace(
+                    mLauncher,
+                    this,
+                    new Point(
+                            launchableCenter.x >= width
+                                    ? launchableCenter.x - width / 2
+                                    : launchableCenter.x + width / 2,
+                            displaySize.y / 2),
+                    getLongPressIndicator());
+        }
     }
 
+    protected abstract void addExpectedEventsForLongClick();
+
     protected abstract String getLongPressIndicator();
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 95c4997..196c6b7 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -79,6 +79,8 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 /**
@@ -92,6 +94,13 @@
     private static final int GESTURE_STEP_MS = 16;
     private static long START_TIME = System.currentTimeMillis();
 
+    static final Pattern LOG_TIME = Pattern.compile(
+            "[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9]");
+
+    static final Pattern EVENT_LOG_ENTRY = Pattern.compile(
+            "(?<time>[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9])"
+                    + ".*" + TestProtocol.TAPL_EVENTS_TAG + ": (?<event>.*)");
+
     // Types for launcher containers that the user is interacting with. "Background" is a
     // pseudo-container corresponding to inactive launcher covered by another app.
     public enum ContainerType {
@@ -146,6 +155,11 @@
 
     private Consumer<ContainerType> mOnSettledStateAction;
 
+    // Not null when we are collecting expected events to compare with actual ones.
+    private List<Pattern> mExpectedEvents;
+
+    private String mTimeBeforeFirstLogEvent;
+
     /**
      * Constructs the root of TAPL hierarchy. You get all other objects from it.
      */
@@ -299,8 +313,11 @@
     public void checkForAnomaly() {
         final String anomalyMessage = getAnomalyMessage();
         if (anomalyMessage != null) {
-            failWithSystemHealth(
-                    "Tests are broken by a non-Launcher system error: " + anomalyMessage);
+            String message = "Tests are broken by a non-Launcher system error: " + anomalyMessage;
+            log("Hierarchy dump for: " + message);
+            dumpViewHierarchy();
+
+            Assert.fail(formatSystemHealthMessage(message));
         }
     }
 
@@ -339,7 +356,7 @@
         mOnSettledStateAction = onSettledStateAction;
     }
 
-    private String getSystemHealthMessage() {
+    private String formatSystemHealthMessage(String message) {
         final String testPackage = getContext().getPackageName();
 
         mInstrumentation.getUiAutomation().grantRuntimePermission(
@@ -347,30 +364,34 @@
         mInstrumentation.getUiAutomation().grantRuntimePermission(
                 testPackage, "android.permission.PACKAGE_USAGE_STATS");
 
-        return mSystemHealthSupplier != null
+        final String systemHealth = mSystemHealthSupplier != null
                 ? mSystemHealthSupplier.apply(START_TIME)
                 : TestHelpers.getSystemHealthMessage(getContext(), START_TIME);
+
+        if (systemHealth != null) {
+            return message
+                    + ",\nperhaps linked to system health problems:\n<<<<<<<<<<<<<<<<<<\n"
+                    + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
+        }
+
+        return message;
     }
 
     private void fail(String message) {
         checkForAnomaly();
 
-        failWithSystemHealth("http://go/tapl : " + getContextDescription() + message +
-                " (visible state: " + getVisibleStateMessage() + ")");
-    }
-
-    private void failWithSystemHealth(String message) {
-        final String systemHealth = getSystemHealthMessage();
-        if (systemHealth != null) {
-            message = message
-                    + ", perhaps because of system health problems:\n<<<<<<<<<<<<<<<<<<\n"
-                    + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
-        }
-
+        message = "http://go/tapl : " + getContextDescription() + message
+                + " (visible state: " + getVisibleStateMessage() + ")";
         log("Hierarchy dump for: " + message);
         dumpViewHierarchy();
 
-        Assert.fail(message);
+        final String eventMismatch = getEventMismatchMessage();
+
+        if (eventMismatch != null) {
+            message = message + ",\nhaving produced wrong events:\n    " + eventMismatch;
+        }
+
+        Assert.fail(formatSystemHealthMessage(message));
     }
 
     private String getContextDescription() {
@@ -582,7 +603,7 @@
             try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
                 mDevice.waitForIdle();
                 runToState(
-                        () -> waitForSystemUiObject("home").click(),
+                        waitForSystemUiObject("home")::click,
                         NORMAL_STATE_ORDINAL,
                         !hasLauncherObject(WORKSPACE_RES_ID)
                                 && (hasLauncherObject(APPS_RES_ID)
@@ -1099,4 +1120,104 @@
         }
         return tasks;
     }
+
+    private List<String> getEvents() {
+        final ArrayList<String> events = new ArrayList<>();
+        try {
+            final String logcatTimeParameter =
+                    mTimeBeforeFirstLogEvent != null ? " -t " + mTimeBeforeFirstLogEvent : "";
+            final String logcatEvents = mDevice.executeShellCommand(
+                    "logcat -d --pid=" + getPid() + logcatTimeParameter
+                            + " -s " + TestProtocol.TAPL_EVENTS_TAG);
+            final Matcher matcher = EVENT_LOG_ENTRY.matcher(logcatEvents);
+            while (matcher.find()) {
+                final String eventTime = matcher.group("time");
+                if (eventTime.equals(mTimeBeforeFirstLogEvent)) continue;
+
+                events.add(matcher.group("event"));
+            }
+            return events;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void startRecordingEvents() {
+        Assert.assertTrue("Already recording events", mExpectedEvents == null);
+        mExpectedEvents = new ArrayList<>();
+
+        try {
+            final String lastLogLine =
+                    mDevice.executeShellCommand("logcat -d --pid=" + getPid() + " -t 1");
+            final Matcher matcher = LOG_TIME.matcher(lastLogLine);
+            mTimeBeforeFirstLogEvent = matcher.find() ? matcher.group().replaceAll(" ", "") : null;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void stopRecordingEvents() {
+        mExpectedEvents = null;
+    }
+
+    Closable eventsCheck() {
+        // Entering events check block.
+        startRecordingEvents();
+
+        return () -> {
+            // Leaving events check block.
+            if (mExpectedEvents == null) {
+                return; // There was a failure. Noo need to report another one.
+            }
+
+            // Wait until Launcher generates expected number of events.
+            final long endTime = SystemClock.uptimeMillis() + WAIT_TIME_MS;
+            while (SystemClock.uptimeMillis() < endTime
+                    && getEvents().size() < mExpectedEvents.size()) {
+                SystemClock.sleep(100);
+            }
+
+            final String message = getEventMismatchMessage();
+            if (message != null) {
+                Assert.fail(formatSystemHealthMessage(
+                        "http://go/tapl : unexpected event sequence: " + message));
+            }
+        };
+    }
+
+    void expectEvent(Pattern expected) {
+        if (mExpectedEvents != null) mExpectedEvents.add(expected);
+    }
+
+    private String getEventMismatchMessage() {
+        if (mExpectedEvents == null) return null;
+
+        try {
+            final List<String> actual = getEvents();
+
+            for (int i = 0; i < mExpectedEvents.size(); ++i) {
+                if (i >= actual.size()) {
+                    return formatEventMismatchMessage("too few actual events", actual, i);
+                }
+                if (!mExpectedEvents.get(i).matcher(actual.get(i)).find()) {
+                    return formatEventMismatchMessage("mismatched event", actual, i);
+                }
+            }
+
+            if (actual.size() > mExpectedEvents.size()) {
+                return formatEventMismatchMessage(
+                        "too many actual events", actual, mExpectedEvents.size());
+            }
+        } finally {
+            stopRecordingEvents();
+        }
+
+        return null;
+    }
+
+    private String formatEventMismatchMessage(String message, List<String> actual, int position) {
+        return message + ", pos=" + position
+                + ", expected=" + mExpectedEvents
+                + ", actual" + actual;
+    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Widget.java b/tests/tapl/com/android/launcher3/tapl/Widget.java
index 1b6d8c4..e0db16d 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widget.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widget.java
@@ -18,10 +18,15 @@
 
 import androidx.test.uiautomator.UiObject2;
 
+import java.util.regex.Pattern;
+
 /**
  * Widget in workspace or a widget list.
  */
 public final class Widget extends Launchable {
+
+    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("Widgets.onLongClick");
+
     Widget(LauncherInstrumentation launcher, UiObject2 icon) {
         super(launcher, icon);
     }
@@ -30,4 +35,9 @@
     protected String getLongPressIndicator() {
         return "drop_target_bar";
     }
+
+    @Override
+    protected void addExpectedEventsForLongClick() {
+        mLauncher.expectEvent(LONG_CLICK_EVENT);
+    }
 }