diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java index 6ecf1c11bd..820a8df521 100644 --- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java @@ -164,7 +164,7 @@ public abstract class RecentsUiFactory { } } - if (FeatureFlags.PULL_DOWN_STATUS_BAR && Utilities.IS_DEBUG_DEVICE + if (FeatureFlags.PULL_DOWN_STATUS_BAR && !launcher.getDeviceProfile().isMultiWindowMode && !launcher.getDeviceProfile().isVerticalBarLayout()) { list.add(new StatusBarTouchController(launcher)); diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java index 53da0f92dd..bf9d531061 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java @@ -43,6 +43,8 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; import android.graphics.Point; import android.graphics.RectF; import android.graphics.Region; @@ -74,6 +76,8 @@ import com.android.launcher3.Utilities; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.logging.EventLogArray; import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.model.AppLaunchTracker; +import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.util.LooperExecutor; import com.android.launcher3.util.UiThreadHelper; import com.android.quickstep.SysUINavigationMode.Mode; @@ -91,6 +95,7 @@ import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer; import com.android.systemui.shared.recents.IOverviewProxy; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.BackgroundExecutor; import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver; import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.InputMonitorCompat; @@ -152,6 +157,7 @@ public class TouchInteractionService extends Service implements .asInterface(bundle.getBinder(KEY_EXTRA_SYSUI_PROXY)); MAIN_THREAD_EXECUTOR.execute(TouchInteractionService.this::initInputMonitor); MAIN_THREAD_EXECUTOR.execute(TouchInteractionService.this::onSystemUiProxySet); + MAIN_THREAD_EXECUTOR.execute(() -> preloadOverview(true /* fromInit */)); } @Override @@ -502,6 +508,9 @@ public class TouchInteractionService extends Service implements } private void onInputEvent(InputEvent ev) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.EVENTS_TO_OVERVIEW_MISSING_TAG, "onInputEvent " + ev); + } if (!(ev instanceof MotionEvent)) { Log.e(TAG, "Unknown event " + ev); return; @@ -684,6 +693,54 @@ public class TouchInteractionService extends Service implements } } + private void preloadOverview(boolean fromInit) { + if (!mIsUserUnlocked) { + return; + } + + final ActivityControlHelper activityControl = + mOverviewComponentObserver.getActivityControlHelper(); + if (activityControl.getCreatedActivity() == null) { + // Make sure that UI states will be initialized. + activityControl.createActivityInitListener((activity, wasVisible) -> { + AppLaunchTracker.INSTANCE.get(activity); + return false; + }).register(); + } else if (fromInit) { + // The activity has been created before the initialization of overview service. It is + // usually happens when booting or launcher is the top activity, so we should already + // have the latest state. + return; + } + + // Pass null animation handler to indicate this start is preload. + BackgroundExecutor.get().submit( + () -> ActivityManagerWrapper.getInstance().startRecentsActivity( + mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState(), + null /* assistDataReceiver */, null /* animationHandler */, + null /* resultCallback */, null /* resultCallbackHandler */)); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + if (!mIsUserUnlocked) { + return; + } + final ActivityControlHelper activityControl = + mOverviewComponentObserver.getActivityControlHelper(); + final BaseDraggingActivity activity = activityControl.getCreatedActivity(); + if (activity == null || activity.isStarted()) { + // We only care about the existing background activity. + return; + } + if (mOverviewComponentObserver.canHandleConfigChanges(activity.getComponentName(), + activity.getResources().getConfiguration().diff(newConfig))) { + return; + } + + preloadOverview(false /* fromInit */); + } + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] rawArgs) { if (rawArgs.length > 0 && Utilities.IS_DEBUG_DEVICE) { diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java index b021df877e..12ea07eaff 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java @@ -19,6 +19,7 @@ import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TI import static com.android.quickstep.TouchInteractionService.TOUCH_INTERACTION_LOG; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; @@ -26,6 +27,7 @@ import androidx.annotation.Nullable; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.Utilities; +import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.views.BaseDragLayer; import com.android.quickstep.OverviewCallbacks; import com.android.systemui.shared.system.ActivityManagerWrapper; @@ -80,6 +82,9 @@ public class OverviewInputConsumer @Override public void onMotionEvent(MotionEvent ev) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.EVENTS_TO_OVERVIEW_MISSING_TAG, "onMotionEvent " + ev); + } if (!mProxyTouch) { return; } diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java index fee18204ea..f5ba3725df 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java @@ -17,17 +17,22 @@ package com.android.launcher3.uioverrides.touchcontrollers; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; +import static android.view.MotionEvent.ACTION_CANCEL; +import android.graphics.PointF; import android.os.RemoteException; import android.util.Log; +import android.util.SparseArray; import android.view.MotionEvent; import android.view.ViewConfiguration; +import android.view.Window; +import android.view.WindowManager; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; -import com.android.launcher3.touch.TouchEventTranslator; import com.android.launcher3.util.TouchController; import com.android.quickstep.RecentsModel; import com.android.systemui.shared.recents.ISystemUiProxy; @@ -36,18 +41,29 @@ import java.io.PrintWriter; /** * TouchController for handling touch events that get sent to the StatusBar. Once the - * Once the event delta y passes the touch slop, the events start getting forwarded. + * Once the event delta mDownY passes the touch slop, the events start getting forwarded. * All events are offset by initial Y value of the pointer. */ public class StatusBarTouchController implements TouchController { private static final String TAG = "StatusBarController"; + /** + * Window flag: Enable touches to slide out of a window into neighboring + * windows in mid-gesture instead of being captured for the duration of + * the gesture. + * + * This flag changes the behavior of touch focus for this window only. + * Touches can slide out of the window but they cannot necessarily slide + * back in (unless the other window with touch focus permits it). + */ + private static final int FLAG_SLIPPERY = 0x20000000; + protected final Launcher mLauncher; - protected final TouchEventTranslator mTranslator; private final float mTouchSlop; private ISystemUiProxy mSysUiProxy; private int mLastAction; + private final SparseArray mDownEvents; /* If {@code false}, this controller should not handle the input {@link MotionEvent}.*/ private boolean mCanIntercept; @@ -56,7 +72,7 @@ public class StatusBarTouchController implements TouchController { mLauncher = l; // Guard against TAPs by increasing the touch slop. mTouchSlop = 2 * ViewConfiguration.get(l).getScaledTouchSlop(); - mTranslator = new TouchEventTranslator((MotionEvent ev)-> dispatchTouchEvent(ev)); + mDownEvents = new SparseArray<>(); } @Override @@ -64,7 +80,6 @@ public class StatusBarTouchController implements TouchController { writer.println(prefix + "mCanIntercept:" + mCanIntercept); writer.println(prefix + "mLastAction:" + MotionEvent.actionToString(mLastAction)); writer.println(prefix + "mSysUiProxy available:" + (mSysUiProxy != null)); - } private void dispatchTouchEvent(MotionEvent ev) { @@ -81,26 +96,31 @@ public class StatusBarTouchController implements TouchController { @Override public final boolean onControllerInterceptTouchEvent(MotionEvent ev) { int action = ev.getActionMasked(); + int idx = ev.getActionIndex(); + int pid = ev.getPointerId(idx); if (action == ACTION_DOWN) { mCanIntercept = canInterceptTouch(ev); if (!mCanIntercept) { return false; } - mTranslator.reset(); - mTranslator.setDownParameters(0, ev); + mDownEvents.put(pid, new PointF(ev.getX(), ev.getY())); } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { - // Check!! should only set it only when threshold is not entered. - mTranslator.setDownParameters(ev.getActionIndex(), ev); + // Check!! should only set it only when threshold is not entered. + mDownEvents.put(pid, new PointF(ev.getX(idx), ev.getY(idx))); } if (!mCanIntercept) { return false; } if (action == ACTION_MOVE) { - float dy = ev.getY() - mTranslator.getDownY(); - float dx = ev.getX() - mTranslator.getDownX(); - if (dy > mTouchSlop && dy > Math.abs(dx)) { - mTranslator.dispatchDownEvents(ev); - mTranslator.processMotionEvent(ev); + float dy = ev.getY(idx) - mDownEvents.get(pid).y; + float dx = ev.getX(idx) - mDownEvents.get(pid).x; + // Currently input dispatcher will not do touch transfer if there are more than + // one touch pointer. Hence, even if slope passed, only set the slippery flag + // when there is single touch event. (context: InputDispatcher.cpp line 1445) + if (dy > mTouchSlop && dy > Math.abs(dx) && ev.getPointerCount() == 1) { + ev.setAction(ACTION_DOWN); + dispatchTouchEvent(ev); + setWindowSlippery(true); return true; } if (Math.abs(dx) > mTouchSlop) { @@ -110,13 +130,27 @@ public class StatusBarTouchController implements TouchController { return false; } - @Override public final boolean onControllerTouchEvent(MotionEvent ev) { - mTranslator.processMotionEvent(ev); + if (ev.getAction() == ACTION_UP || ev.getAction() == ACTION_CANCEL) { + dispatchTouchEvent(ev); + setWindowSlippery(false); + return true; + } return true; } + private void setWindowSlippery(boolean enable) { + Window w = mLauncher.getWindow(); + WindowManager.LayoutParams wlp = w.getAttributes(); + if (enable) { + wlp.flags |= FLAG_SLIPPERY; + } else { + wlp.flags &= ~FLAG_SLIPPERY; + } + w.setAttributes(wlp); + } + private boolean canInterceptTouch(MotionEvent ev) { if (!mLauncher.isInState(LauncherState.NORMAL) || AbstractFloatingView.getTopOpenViewWithType(mLauncher, @@ -132,4 +166,4 @@ public class StatusBarTouchController implements TouchController { mSysUiProxy = RecentsModel.INSTANCE.get(mLauncher).getSystemUiProxy(); return mSysUiProxy != null; } -} +} \ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java index 0738affa93..4a2ed3a3d1 100644 --- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java +++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java @@ -29,11 +29,15 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.util.SparseIntArray; import com.android.systemui.shared.system.PackageManagerWrapper; import java.util.ArrayList; +import java.util.Objects; /** * Class to keep track of the current overview component based off user preferences and app updates @@ -53,22 +57,41 @@ public final class OverviewComponentObserver { } }; private final Context mContext; - private final ComponentName mMyHomeComponent; + private final Intent mCurrentHomeIntent; + private final Intent mMyHomeIntent; + private final Intent mFallbackIntent; + private final SparseIntArray mConfigChangesMap = new SparseIntArray(); private String mUpdateRegisteredPackage; private ActivityControlHelper mActivityControlHelper; private Intent mOverviewIntent; - private Intent mHomeIntent; private int mSystemUiStateFlags; private boolean mIsHomeAndOverviewSame; + private boolean mIsDefaultHome; public OverviewComponentObserver(Context context) { mContext = context; - Intent myHomeIntent = new Intent(Intent.ACTION_MAIN) + mCurrentHomeIntent = new Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_HOME) - .setPackage(mContext.getPackageName()); - ResolveInfo info = context.getPackageManager().resolveActivity(myHomeIntent, 0); - mMyHomeComponent = new ComponentName(context.getPackageName(), info.activityInfo.name); + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mMyHomeIntent = new Intent(mCurrentHomeIntent).setPackage(mContext.getPackageName()); + ResolveInfo info = context.getPackageManager().resolveActivity(mMyHomeIntent, 0); + ComponentName myHomeComponent = + new ComponentName(context.getPackageName(), info.activityInfo.name); + mMyHomeIntent.setComponent(myHomeComponent); + mConfigChangesMap.append(myHomeComponent.hashCode(), info.activityInfo.configChanges); + + ComponentName fallbackComponent = new ComponentName(mContext, RecentsActivity.class); + mFallbackIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_DEFAULT) + .setComponent(fallbackComponent) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + try { + ActivityInfo fallbackInfo = context.getPackageManager().getActivityInfo( + mFallbackIntent.getComponent(), 0 /* flags */); + mConfigChangesMap.append(fallbackComponent.hashCode(), fallbackInfo.configChanges); + } catch (PackageManager.NameNotFoundException ignored) { /* Impossible */ } mContext.registerReceiver(mUserPreferenceChangeReceiver, new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED)); @@ -92,17 +115,14 @@ public final class OverviewComponentObserver { ComponentName defaultHome = PackageManagerWrapper.getInstance() .getHomeActivities(new ArrayList<>()); - final String overviewIntentCategory; - ComponentName overviewComponent; - mHomeIntent = null; - - if ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0 && - (defaultHome == null || mMyHomeComponent.equals(defaultHome))) { + mIsDefaultHome = Objects.equals(mMyHomeIntent.getComponent(), defaultHome); + if ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0 + && (defaultHome == null || mIsDefaultHome)) { // User default home is same as out home app. Use Overview integrated in Launcher. - overviewComponent = mMyHomeComponent; mActivityControlHelper = new LauncherActivityControllerHelper(); mIsHomeAndOverviewSame = true; - overviewIntentCategory = Intent.CATEGORY_HOME; + mOverviewIntent = mMyHomeIntent; + mCurrentHomeIntent.setComponent(mMyHomeIntent.getComponent()); if (mUpdateRegisteredPackage != null) { // Remove any update listener as we don't care about other packages. @@ -111,14 +131,11 @@ public final class OverviewComponentObserver { } } else { // The default home app is a different launcher. Use the fallback Overview instead. - overviewComponent = new ComponentName(mContext, RecentsActivity.class); mActivityControlHelper = new FallbackActivityControllerHelper(); mIsHomeAndOverviewSame = false; - overviewIntentCategory = Intent.CATEGORY_DEFAULT; + mOverviewIntent = mFallbackIntent; + mCurrentHomeIntent.setComponent(defaultHome); - mHomeIntent = new Intent(Intent.ACTION_MAIN) - .addCategory(Intent.CATEGORY_HOME) - .setComponent(defaultHome); // User's default home app can change as a result of package updates of this app (such // as uninstalling the app or removing the "Launcher" feature in an update). // Listen for package updates of this app (and remove any previously attached @@ -138,14 +155,6 @@ public final class OverviewComponentObserver { ACTION_PACKAGE_REMOVED)); } } - - mOverviewIntent = new Intent(Intent.ACTION_MAIN) - .addCategory(overviewIntentCategory) - .setComponent(overviewComponent) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (mHomeIntent == null) { - mHomeIntent = mOverviewIntent; - } } /** @@ -160,6 +169,32 @@ public final class OverviewComponentObserver { } } + /** + * @return {@code true} if the overview component is able to handle the configuration changes. + */ + boolean canHandleConfigChanges(ComponentName component, int changes) { + final int orientationChange = + ActivityInfo.CONFIG_ORIENTATION | ActivityInfo.CONFIG_SCREEN_SIZE; + if ((changes & orientationChange) == orientationChange) { + // This is just an approximate guess for simple orientation change because the changes + // may contain non-public bits (e.g. window configuration). + return true; + } + + int configMask = mConfigChangesMap.get(component.hashCode()); + return configMask != 0 && (~configMask & changes) == 0; + } + + /** + * Get the intent for overview activity. It is used when lockscreen is shown and home was died + * in background, we still want to restart the one that will be used after unlock. + * + * @return the overview intent + */ + Intent getOverviewIntentIgnoreSysUiState() { + return mIsDefaultHome ? mMyHomeIntent : mOverviewIntent; + } + /** * Get the current intent for going to the overview activity. * @@ -173,7 +208,7 @@ public final class OverviewComponentObserver { * Get the current intent for going to the home activity. */ public Intent getHomeIntent() { - return mHomeIntent; + return mCurrentHomeIntent; } /** diff --git a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java index 0c5a6f5b6e..ec3d49afad 100644 --- a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java +++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java @@ -51,7 +51,7 @@ public class DigitalWellBeingToastTest extends AbstractQuickStepTest { mLauncher.pressHome(); final DigitalWellBeingToast toast = getToast(); - assertTrue("Toast is not visible", toast.hasLimit()); + waitForLauncherCondition("Toast is not visible", launcher -> toast.hasLimit()); assertEquals("Toast text: ", "5 minutes left today", toast.getText()); // Unset time limit for app. diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java index 2111e2ca27..c5b560c3fc 100644 --- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java +++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java @@ -25,6 +25,7 @@ import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.Launcher; +import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.util.RaceConditionReproducer; import com.android.quickstep.NavigationModeSwitchRule.Mode; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; @@ -79,6 +80,8 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest { @Test @NavigationModeSwitch public void testStressPressHome() { + if (LauncherInstrumentation.isAvd()) return; // b/136278866 + for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) { // Destroy Launcher activity. closeLauncherActivity(); @@ -91,6 +94,8 @@ public class StartLauncherViaGestureTests extends AbstractQuickStepTest { @Test @NavigationModeSwitch public void testStressSwipeToOverview() { + if (LauncherInstrumentation.isAvd()) return; // b/136278866 + for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) { // Destroy Launcher activity. closeLauncherActivity(); diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java index 6ffc2d9cd1..3774042c93 100644 --- a/src/com/android/launcher3/testing/TestProtocol.java +++ b/src/com/android/launcher3/testing/TestProtocol.java @@ -78,4 +78,5 @@ public final class TestProtocol { public static final String NO_START_TAG = "b/132900132"; public static final String NO_START_TASK_TAG = "b/133765434"; public static final String NO_OVERVIEW_EVENT_TAG = "b/134532571"; + public static final String EVENTS_TO_OVERVIEW_MISSING_TAG = "b/133867119"; } diff --git a/src/com/android/launcher3/touch/TouchEventTranslator.java b/src/com/android/launcher3/touch/TouchEventTranslator.java deleted file mode 100644 index 3fcda90848..0000000000 --- a/src/com/android/launcher3/touch/TouchEventTranslator.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * 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.launcher3.touch; - -import android.graphics.PointF; -import android.util.Log; -import android.util.Pair; -import android.util.SparseArray; -import android.view.MotionEvent; -import android.view.MotionEvent.PointerCoords; -import android.view.MotionEvent.PointerProperties; - -import java.util.function.Consumer; - -/** - * To minimize the size of the MotionEvent, historic events are not copied and passed via the - * listener. - */ -public class TouchEventTranslator { - - private static final String TAG = "TouchEventTranslator"; - private static final boolean DEBUG = false; - - private class DownState { - long timeStamp; - float downX; - float downY; - public DownState(long timeStamp, float downX, float downY) { - this.timeStamp = timeStamp; - this.downX = downX; - this.downY = downY; - } - }; - private final DownState ZERO = new DownState(0, 0f, 0f); - - private final Consumer mListener; - - private final SparseArray mDownEvents; - private final SparseArray mFingers; - - private final SparseArray> mCache; - - public TouchEventTranslator(Consumer listener) { - mDownEvents = new SparseArray<>(); - mFingers = new SparseArray<>(); - mCache = new SparseArray<>(); - - mListener = listener; - } - - public void reset() { - mDownEvents.clear(); - mFingers.clear(); - } - - public float getDownX() { - return mDownEvents.get(0).downX; - } - - public float getDownY() { - return mDownEvents.get(0).downY; - } - - public void setDownParameters(int idx, MotionEvent e) { - DownState ev = new DownState(e.getEventTime(), e.getX(idx), e.getY(idx)); - mDownEvents.append(idx, ev); - } - - public void dispatchDownEvents(MotionEvent ev) { - for(int i = 0; i < ev.getPointerCount() && i < mDownEvents.size(); i++) { - int pid = ev.getPointerId(i); - put(pid, i, ev.getX(i), 0, mDownEvents.get(i).timeStamp, ev); - } - } - - public void processMotionEvent(MotionEvent ev) { - if (DEBUG) { - printSamples(TAG + " processMotionEvent", ev); - } - int index = ev.getActionIndex(); - float x = ev.getX(index); - float y = ev.getY(index) - mDownEvents.get(index, ZERO).downY; - switch (ev.getActionMasked()) { - case MotionEvent.ACTION_POINTER_DOWN: - int pid = ev.getPointerId(index); - if(mFingers.get(pid, null) != null) { - for(int i=0; i < ev.getPointerCount(); i++) { - pid = ev.getPointerId(i); - position(pid, x, y); - } - generateEvent(ev.getAction(), ev); - } else { - put(pid, index, x, y, ev); - } - break; - case MotionEvent.ACTION_MOVE: - for(int i=0; i < ev.getPointerCount(); i++) { - pid = ev.getPointerId(i); - position(pid, x, y); - } - generateEvent(ev.getAction(), ev); - break; - case MotionEvent.ACTION_POINTER_UP: - case MotionEvent.ACTION_UP: - pid = ev.getPointerId(index); - lift(pid, index, x, y, ev); - break; - case MotionEvent.ACTION_CANCEL: - cancel(ev); - break; - default: - Log.v(TAG, "Didn't process "); - printSamples(TAG, ev); - - } - } - - private TouchEventTranslator put(int id, int index, float x, float y, MotionEvent ev) { - return put(id, index, x, y, ev.getEventTime(), ev); - } - - private TouchEventTranslator put(int id, int index, float x, float y, long ms, MotionEvent ev) { - checkFingerExistence(id, false); - boolean isInitialDown = (mFingers.size() == 0); - - mFingers.put(id, new PointF(x, y)); - int n = mFingers.size(); - - if (mCache.get(n) == null) { - PointerProperties[] properties = new PointerProperties[n]; - PointerCoords[] coords = new PointerCoords[n]; - for (int i = 0; i < n; i++) { - properties[i] = new PointerProperties(); - coords[i] = new PointerCoords(); - } - mCache.put(n, new Pair(properties, coords)); - } - - int action; - if (isInitialDown) { - action = MotionEvent.ACTION_DOWN; - } else { - action = MotionEvent.ACTION_POINTER_DOWN; - // Set the id of the changed pointer. - action |= index << MotionEvent.ACTION_POINTER_INDEX_SHIFT; - } - generateEvent(action, ms, ev); - return this; - } - - public TouchEventTranslator position(int id, float x, float y) { - checkFingerExistence(id, true); - mFingers.get(id).set(x, y); - return this; - } - - private TouchEventTranslator lift(int id, int index, MotionEvent ev) { - checkFingerExistence(id, true); - boolean isFinalUp = (mFingers.size() == 1); - int action; - if (isFinalUp) { - action = MotionEvent.ACTION_UP; - } else { - action = MotionEvent.ACTION_POINTER_UP; - // Set the id of the changed pointer. - action |= index << MotionEvent.ACTION_POINTER_INDEX_SHIFT; - } - generateEvent(action, ev); - mFingers.remove(id); - return this; - } - - private TouchEventTranslator lift(int id, int index, float x, float y, MotionEvent ev) { - checkFingerExistence(id, true); - mFingers.get(id).set(x, y); - return lift(id, index, ev); - } - - public TouchEventTranslator cancel(MotionEvent ev) { - generateEvent(MotionEvent.ACTION_CANCEL, ev); - mFingers.clear(); - return this; - } - - private void checkFingerExistence(int id, boolean shouldExist) { - if (shouldExist != (mFingers.get(id, null) != null)) { - throw new IllegalArgumentException( - shouldExist ? "Finger does not exist" : "Finger already exists"); - } - } - - - /** - * Used to debug MotionEvents being sent/received. - */ - public void printSamples(String msg, MotionEvent ev) { - System.out.printf("%s %s", msg, MotionEvent.actionToString(ev.getActionMasked())); - final int pointerCount = ev.getPointerCount(); - System.out.printf("#%d/%d", ev.getActionIndex(), pointerCount); - System.out.printf(" t=%d:", ev.getEventTime()); - for (int p = 0; p < pointerCount; p++) { - System.out.printf(" id=%d: (%f,%f)", - ev.getPointerId(p), ev.getX(p), ev.getY(p)); - } - System.out.println(); - } - - private void generateEvent(int action, MotionEvent ev) { - generateEvent(action, ev.getEventTime(), ev); - } - - private void generateEvent(int action, long ms, MotionEvent ev) { - Pair state = getFingerState(); - MotionEvent event = MotionEvent.obtain( - mDownEvents.get(0).timeStamp, - ms, - action, - state.first.length, - state.first, - state.second, - ev.getMetaState(), - ev.getButtonState() /* buttonState */, - ev.getXPrecision() /* xPrecision */, - ev.getYPrecision() /* yPrecision */, - ev.getDeviceId(), - ev.getEdgeFlags(), - ev.getSource(), - ev.getFlags() /* flags */); - if (DEBUG) { - printSamples(TAG + " generateEvent", event); - } - if (event.getPointerId(event.getActionIndex()) < 0) { - printSamples(TAG + "generateEvent", event); - throw new IllegalStateException(event.getActionIndex() + " not found in MotionEvent"); - } - mListener.accept(event); - event.recycle(); - } - - /** - * Returns the description of the fingers' state expected by MotionEvent. - */ - private Pair getFingerState() { - int nFingers = mFingers.size(); - - Pair result = mCache.get(nFingers); - PointerProperties[] properties = result.first; - PointerCoords[] coordinates = result.second; - - int index = 0; - for (int i = 0; i < mFingers.size(); i++) { - int id = mFingers.keyAt(i); - PointF location = mFingers.get(id); - - PointerProperties property = properties[i]; - property.id = id; - property.toolType = MotionEvent.TOOL_TYPE_FINGER; - properties[index] = property; - - PointerCoords coordinate = coordinates[i]; - coordinate.x = location.x; - coordinate.y = location.y; - coordinate.pressure = 1.0f; - coordinates[index] = coordinate; - - index++; - } - return mCache.get(nFingers); - } -} diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java index c1ba780503..53686cae48 100644 --- a/src/com/android/launcher3/views/BaseDragLayer.java +++ b/src/com/android/launcher3/views/BaseDragLayer.java @@ -152,6 +152,9 @@ public abstract class BaseDragLayer } private TouchController findControllerToHandleTouch(MotionEvent ev) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.EVENTS_TO_OVERVIEW_MISSING_TAG, "findControllerToHandleTouch " + ev); + } if (shouldDisableGestures(ev)) return null; AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); @@ -314,6 +317,9 @@ public abstract class BaseDragLayer * Proxies the touch events to the gesture handlers */ public boolean proxyTouchEvent(MotionEvent ev) { + if (TestProtocol.sDebugTracing) { + Log.d(TestProtocol.EVENTS_TO_OVERVIEW_MISSING_TAG, "proxyTouchEvent " + ev); + } boolean handled; if (mProxyTouchController != null) { handled = mProxyTouchController.onControllerTouchEvent(ev); diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java index 4a0ca5c245..e39fc76a0b 100644 --- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java +++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java @@ -20,7 +20,6 @@ import static androidx.test.InstrumentationRegistry.getInstrumentation; import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static java.lang.System.exit; @@ -38,10 +37,7 @@ import android.util.Log; import androidx.test.InstrumentationRegistry; import androidx.test.uiautomator.By; -import androidx.test.uiautomator.BySelector; -import androidx.test.uiautomator.Direction; import androidx.test.uiautomator.UiDevice; -import androidx.test.uiautomator.UiObject2; import androidx.test.uiautomator.Until; import com.android.launcher3.Launcher; @@ -50,12 +46,12 @@ import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherState; import com.android.launcher3.MainThreadExecutor; -import com.android.launcher3.ResourceUtils; import com.android.launcher3.Utilities; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.model.AppLaunchTracker; import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.tapl.TestHelpers; +import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.FailureWatcher; import com.android.launcher3.util.rule.LauncherActivityRule; @@ -86,8 +82,7 @@ public abstract class AbstractLauncherUiTest { public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10); public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5; - public static final long SHORT_UI_TIMEOUT = 300; - public static final long DEFAULT_UI_TIMEOUT = 10000; + public static final long DEFAULT_UI_TIMEOUT = 60000; // b/136278866 private static final String TAG = "AbstractLauncherUiTest"; protected MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor(); @@ -116,6 +111,23 @@ public abstract class AbstractLauncherUiTest { public ShellCommandRule mDisableHeadsUpNotification = ShellCommandRule.disableHeadsUpNotification(); + protected void clearPackageData(String pkg) throws IOException, InterruptedException { + final CountDownLatch count = new CountDownLatch(2); + final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + count.countDown(); + } + }; + mTargetContext.registerReceiver(broadcastReceiver, + PackageManagerHelper.getPackageFilter(pkg, + Intent.ACTION_PACKAGE_RESTARTED, Intent.ACTION_PACKAGE_DATA_CLEARED)); + + mDevice.executeShellCommand("pm clear " + pkg); + assertTrue(pkg + " didn't restart", count.await(10, TimeUnit.SECONDS)); + mTargetContext.unregisterReceiver(broadcastReceiver); + } + // Annotation for tests that need to be run in portrait and landscape modes. @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @@ -170,38 +182,13 @@ public abstract class AbstractLauncherUiTest { } } - protected void clearLauncherData() throws IOException { + protected void clearLauncherData() throws IOException, InterruptedException { if (TestHelpers.isInLauncherProcess()) { LauncherSettings.Settings.call(mTargetContext.getContentResolver(), LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); resetLoaderState(); } else { - mDevice.executeShellCommand("pm clear " + mDevice.getLauncherPackageName()); - } - } - - /** - * Scrolls the {@param container} until it finds an object matching {@param condition}. - * - * @return the matching object. - */ - protected UiObject2 scrollAndFind(UiObject2 container, BySelector condition) { - final int margin = ResourceUtils.getNavbarSize( - ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources()) + 1; - container.setGestureMargins(0, 0, 0, margin); - - int i = 0; - for (; ; ) { - // findObject can only execute after spring settles. - mDevice.wait(Until.findObject(condition), SHORT_UI_TIMEOUT); - UiObject2 widget = container.findObject(condition); - if (widget != null && widget.getVisibleBounds().intersects( - 0, 0, mDevice.getDisplayWidth(), - mDevice.getDisplayHeight() - margin)) { - return widget; - } - if (++i > 40) fail("Too many attempts"); - container.scroll(Direction.DOWN, 1f); + clearPackageData(mDevice.getLauncherPackageName()); } } diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index c3168f812e..4dab44fadd 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -216,7 +216,8 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { final AppIcon app = allApps.getAppIcon("TestActivity7"); assertNotNull("AppIcon.launch returned null", app.launch(getAppPackageName())); test.executeOnLauncher(launcher -> assertTrue( - "Launcher activity is the top activity; expecting another activity to be the top " + "Launcher activity is the top activity; expecting another activity to be the " + + "top " + "one", test.isInBackground(launcher))); } finally { @@ -304,11 +305,8 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { switchToAllApps(); allApps.freeze(); try { - allApps. - getAppIcon(APP_NAME). - dragToWorkspace(). - getWorkspaceAppIcon(APP_NAME). - launch(getAppPackageName()); + allApps.getAppIcon(APP_NAME).dragToWorkspace(); + mLauncher.getWorkspace().getWorkspaceAppIcon(APP_NAME).launch(getAppPackageName()); } finally { allApps.unfreeze(); } @@ -335,13 +333,8 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { getMenuItem(0); final String shortcutName = menuItem.getText(); - // 4. Drag the first shortcut to the home screen. - // 5. Verify that the shortcut works on home screen - // (the app opens and has the same text as the shortcut). - menuItem. - dragToWorkspace(). - getWorkspaceAppIcon(shortcutName). - launch(getAppPackageName()); + menuItem.dragToWorkspace(); + mLauncher.getWorkspace().getWorkspaceAppIcon(shortcutName).launch(getAppPackageName()); } finally { allApps.unfreeze(); } diff --git a/tests/src/com/android/launcher3/ui/TestViewHelpers.java b/tests/src/com/android/launcher3/ui/TestViewHelpers.java index d13d319525..d0df66485e 100644 --- a/tests/src/com/android/launcher3/ui/TestViewHelpers.java +++ b/tests/src/com/android/launcher3/ui/TestViewHelpers.java @@ -18,28 +18,15 @@ package com.android.launcher3.ui; import static androidx.test.InstrumentationRegistry.getInstrumentation; import static androidx.test.InstrumentationRegistry.getTargetContext; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - import android.content.ComponentName; -import android.content.Context; -import android.graphics.Point; import android.os.Process; -import android.os.SystemClock; import android.util.Log; -import android.view.KeyEvent; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import androidx.test.uiautomator.By; -import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.UiDevice; -import androidx.test.uiautomator.UiObject2; -import androidx.test.uiautomator.Until; import com.android.launcher3.LauncherAppWidgetProviderInfo; -import com.android.launcher3.R; import com.android.launcher3.compat.AppWidgetManagerCompat; import com.android.launcher3.testcomponent.AppWidgetNoConfig; import com.android.launcher3.testcomponent.AppWidgetWithConfig; @@ -50,21 +37,6 @@ import java.util.function.Function; public class TestViewHelpers { private static final String TAG = "TestViewHelpers"; - private static UiDevice getDevice() { - return UiDevice.getInstance(getInstrumentation()); - } - - public static UiObject2 findViewById(int id) { - return getDevice().wait(Until.findObject(getSelectorForId(id)), - AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT); - } - - public static BySelector getSelectorForId(int id) { - final Context targetContext = getTargetContext(); - String name = targetContext.getResources().getResourceEntryName(id); - return By.res(targetContext.getPackageName(), name); - } - /** * Finds a widget provider which can fit on the home screen. * @@ -91,92 +63,6 @@ public class TestViewHelpers { return info; } - /** - * Drags an icon to the center of homescreen. - * - * @param icon object that is either app icon or shortcut icon - */ - public static void dragToWorkspace(UiObject2 icon, boolean expectedToShowShortcuts) { - Point center = icon.getVisibleCenter(); - - // Action Down - final long downTime = SystemClock.uptimeMillis(); - sendPointer(downTime, MotionEvent.ACTION_DOWN, center); - - UiObject2 dragLayer = findViewById(R.id.drag_layer); - - if (expectedToShowShortcuts) { - // Make sure shortcuts show up, and then move a bit to hide them. - assertNotNull(findViewById(R.id.deep_shortcuts_container)); - - Point moveLocation = new Point(center); - int distanceToMove = - getTargetContext().getResources().getDimensionPixelSize( - R.dimen.deep_shortcuts_start_drag_threshold) + 50; - if (moveLocation.y - distanceToMove >= dragLayer.getVisibleBounds().top) { - moveLocation.y -= distanceToMove; - } else { - moveLocation.y += distanceToMove; - } - movePointer(downTime, center, moveLocation); - - assertNull(findViewById(R.id.deep_shortcuts_container)); - } - - // Wait until Remove/Delete target is visible - assertNotNull(findViewById(R.id.delete_target_text)); - - Point moveLocation = dragLayer.getVisibleCenter(); - - // Move to center - movePointer(downTime, center, moveLocation); - sendPointer(downTime, MotionEvent.ACTION_UP, moveLocation); - - // Wait until remove target is gone. - getDevice().wait(Until.gone(getSelectorForId(R.id.delete_target_text)), - AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT); - } - - private static void movePointer(long downTime, Point from, Point to) { - while (!from.equals(to)) { - try { - Thread.sleep(20); - } catch (InterruptedException e) { - e.printStackTrace(); - } - from.x = getNextMoveValue(to.x, from.x); - from.y = getNextMoveValue(to.y, from.y); - sendPointer(downTime, MotionEvent.ACTION_MOVE, from); - } - } - - private static int getNextMoveValue(int targetValue, int oldValue) { - if (targetValue - oldValue > 10) { - return oldValue + 10; - } else if (targetValue - oldValue < -10) { - return oldValue - 10; - } else { - return targetValue; - } - } - - public static void sendPointer(long downTime, int action, Point point) { - MotionEvent event = MotionEvent.obtain(downTime, - SystemClock.uptimeMillis(), action, point.x, point.y, 0); - getInstrumentation().sendPointerSync(event); - event.recycle(); - } - - /** - * Opens widget tray and returns the recycler view. - */ - public static UiObject2 openWidgetsTray() { - final UiDevice device = getDevice(); - device.pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON); - device.waitForIdle(); - return findViewById(R.id.widgets_list_view); - } - public static View findChildView(ViewGroup parent, Function condition) { for (int i = 0; i < parent.getChildCount(); ++i) { final View child = parent.getChildAt(i); diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java index 5eb5f19928..dc72bda9a3 100644 --- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java +++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java @@ -27,20 +27,18 @@ import android.view.View; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; -import androidx.test.uiautomator.By; -import androidx.test.uiautomator.UiObject2; import com.android.launcher3.ItemInfo; import com.android.launcher3.LauncherAppWidgetInfo; import com.android.launcher3.LauncherAppWidgetProviderInfo; import com.android.launcher3.Workspace; +import com.android.launcher3.tapl.Widgets; import com.android.launcher3.testcomponent.WidgetConfigActivity; import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.ui.TestViewHelpers; import com.android.launcher3.util.Condition; import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.ShellCommandRule; -import com.android.launcher3.widget.WidgetCell; import org.junit.Before; import org.junit.Ignore; @@ -71,7 +69,6 @@ public class AddConfigWidgetTest extends AbstractLauncherUiTest { } @Test - // Convert test to TAPL b/131116002 public void testWidgetConfig() throws Throwable { runTest(false, true); } @@ -83,7 +80,6 @@ public class AddConfigWidgetTest extends AbstractLauncherUiTest { } @Test - // Convert test to TAPL b/131116002 public void testConfigCancelled() throws Throwable { runTest(false, false); } @@ -104,15 +100,13 @@ public class AddConfigWidgetTest extends AbstractLauncherUiTest { clearHomescreen(); mActivityMonitor.startLauncher(); - // Open widget tray and wait for load complete. - final UiObject2 widgetContainer = TestViewHelpers.openWidgetsTray(); - Wait.atMost(null, Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT); + final Widgets widgets = mLauncher.getWorkspace().openAllWidgets(); // Drag widget to homescreen WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor(); - UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class) - .hasDescendant(By.text(mWidgetInfo.getLabel(mTargetContext.getPackageManager())))); - TestViewHelpers.dragToWorkspace(widget, false); + widgets. + getWidget(mWidgetInfo.getLabel(mTargetContext.getPackageManager())). + dragToWorkspace(); // Widget id for which the config activity was opened mWidgetId = monitor.getWidgetId(); diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java index 0061568cce..4529a80d1d 100644 --- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java +++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java @@ -19,20 +19,12 @@ import static org.junit.Assert.assertTrue; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; -import androidx.test.uiautomator.By; -import androidx.test.uiautomator.UiObject2; -import android.view.View; -import com.android.launcher3.ItemInfo; import com.android.launcher3.LauncherAppWidgetInfo; import com.android.launcher3.LauncherAppWidgetProviderInfo; -import com.android.launcher3.Workspace.ItemOperator; import com.android.launcher3.ui.AbstractLauncherUiTest; import com.android.launcher3.ui.TestViewHelpers; -import com.android.launcher3.util.Condition; -import com.android.launcher3.util.Wait; import com.android.launcher3.util.rule.ShellCommandRule; -import com.android.launcher3.widget.WidgetCell; import org.junit.Ignore; import org.junit.Rule; @@ -61,7 +53,6 @@ public class AddWidgetTest extends AbstractLauncherUiTest { performTest(); } - // Convert to TAPL b/131116002 private void performTest() throws Throwable { clearHomescreen(); mActivityMonitor.startLauncher(); @@ -69,22 +60,15 @@ public class AddWidgetTest extends AbstractLauncherUiTest { final LauncherAppWidgetProviderInfo widgetInfo = TestViewHelpers.findWidgetProvider(this, false /* hasConfigureScreen */); - // Open widget tray and wait for load complete. - final UiObject2 widgetContainer = TestViewHelpers.openWidgetsTray(); - Wait.atMost(null, Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT); + mLauncher. + getWorkspace(). + openAllWidgets(). + getWidget(widgetInfo.getLabel(mTargetContext.getPackageManager())). + dragToWorkspace(); - // Drag widget to homescreen - UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class) - .hasDescendant(By.text(widgetInfo.getLabel(mTargetContext.getPackageManager())))); - TestViewHelpers.dragToWorkspace(widget, false); - - assertTrue(mActivityMonitor.itemExists(new ItemOperator() { - @Override - public boolean evaluate(ItemInfo info, View view) { - return info instanceof LauncherAppWidgetInfo && + assertTrue(mActivityMonitor.itemExists( + (info, view) -> info instanceof LauncherAppWidgetInfo && ((LauncherAppWidgetInfo) info).providerName.getClassName().equals( - widgetInfo.provider.getClassName()); - } - }).call()); + widgetInfo.provider.getClassName())).call()); } } diff --git a/tests/tapl/AndroidManifest.xml b/tests/tapl/AndroidManifest.xml index 0207e2be6f..1065446827 100644 --- a/tests/tapl/AndroidManifest.xml +++ b/tests/tapl/AndroidManifest.xml @@ -23,4 +23,6 @@ > + + diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java index 9ff354a7eb..0ac5e9f887 100644 --- a/tests/tapl/com/android/launcher3/tapl/AllApps.java +++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java @@ -181,6 +181,7 @@ public class AllApps extends LauncherInstrumentation.VisibleContainer { * Flings backward (up) and waits the fling's end. */ public void flingBackward() { + mLauncher.getTestInfo(TestProtocol.REQUEST_ENABLE_DEBUG_TRACING); try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("want to fling backward in all apps")) { final UiObject2 allAppsContainer = verifyActiveContainer(); @@ -189,6 +190,7 @@ public class AllApps extends LauncherInstrumentation.VisibleContainer { allAppsContainer, Direction.UP, 1, new Rect(0, mHeight / 2, 0, 0), 10); verifyActiveContainer(); } + mLauncher.getTestInfo(TestProtocol.REQUEST_DISABLE_DEBUG_TRACING); } /** diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java index d4bdafa764..04b8019450 100644 --- a/tests/tapl/com/android/launcher3/tapl/Launchable.java +++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java @@ -68,7 +68,7 @@ abstract class Launchable { /** * Drags an object to the center of homescreen. */ - public Workspace dragToWorkspace() { + public void dragToWorkspace() { final Point launchableCenter = getObject().getVisibleCenter(); final Point displaySize = mLauncher.getRealDisplaySize(); final int width = displaySize.x / 2; @@ -80,10 +80,6 @@ abstract class Launchable { launchableCenter.x - width / 2 : launchableCenter.x + width / 2, displaySize.y / 2), getLongPressIndicator()); - try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( - "dragged launchable to workspace")) { - return new Workspace(mLauncher); - } } 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 a7e6336199..11b0665d95 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -39,6 +39,7 @@ import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.DropBoxManager; import android.os.Parcelable; import android.os.SystemClock; import android.text.TextUtils; @@ -263,10 +264,122 @@ public final class LauncherInstrumentation { } } + private String getAnomalyMessage() { + final UiObject2 object = mDevice.findObject(By.res("android", "alertTitle")); + if (object != null) { + return "System alert popup is visible: " + object.getText(); + } + + if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked"; + + if (!mDevice.hasObject(By.textStartsWith(""))) return "Screen is empty"; + + return null; + } + + private String getVisibleStateMessage() { + if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets"; + if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview"; + if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace"; + if (hasLauncherObject(APPS_RES_ID)) return "AllApps"; + return "Background"; + } + + private static String truncateCrash(String text, int maxLines) { + String[] lines = text.split("\\r?\\n"); + StringBuilder ret = new StringBuilder(); + for (int i = 0; i < maxLines && i < lines.length; i++) { + ret.append(lines[i]); + ret.append('\n'); + } + if (lines.length > maxLines) { + ret.append("... "); + ret.append(lines.length - maxLines); + ret.append(" more lines truncated ...\n"); + } + return ret.toString(); + } + + private String checkCrash(String label) { + DropBoxManager dropbox = (DropBoxManager) getContext().getSystemService( + Context.DROPBOX_SERVICE); + Assert.assertNotNull("Unable access the DropBoxManager service", dropbox); + + long timestamp = 0; + DropBoxManager.Entry entry; + int crashCount = 0; + StringBuilder errorDetails = new StringBuilder(); + while (null != (entry = dropbox.getNextEntry(label, timestamp))) { + String dropboxSnippet; + try { + dropboxSnippet = entry.getText(4096); + } finally { + entry.close(); + } + + crashCount++; + errorDetails.append(label); + errorDetails.append(": "); + errorDetails.append(truncateCrash(dropboxSnippet, 40)); + errorDetails.append(" ...\n"); + + timestamp = entry.getTimeMillis(); + } + Assert.assertEquals(errorDetails.toString(), 0, crashCount); + return crashCount > 0 ? errorDetails.toString() : null; + } + + private String getSystemHealthMessage() { + try { + StringBuilder errors = new StringBuilder(); + + final String testPackage = getContext().getPackageName(); + try { + mDevice.executeShellCommand("pm grant " + testPackage + + " android.permission.READ_LOGS"); + mDevice.executeShellCommand("pm grant " + testPackage + + " android.permission.PACKAGE_USAGE_STATS"); + } catch (IOException e) { + e.printStackTrace(); + } + + final String[] labels = { + "system_server_crash", + "system_server_native_crash", + "system_server_anr", + }; + + for (String label : labels) { + final String crash = checkCrash(label); + if (crash != null) errors.append(crash); + } + + return errors.length() != 0 ? errors.toString() : null; + } catch (Exception e) { + return null; + } + } + private void fail(String message) { - log("Hierarchy dump for: " + getContextDescription() + message); + message = "http://go/tapl : " + getContextDescription() + message; + + final String anomaly = getAnomalyMessage(); + if (anomaly != null) { + message = anomaly + ", which causes:\n" + message; + } else { + message = message + " (visible state: " + getVisibleStateMessage() + ")"; + } + + final String systemHealth = getSystemHealthMessage(); + if (systemHealth != null) { + message = message + ", which might be a consequence of system health problems:\n<<<\n" + + systemHealth + "\n>>>"; + } + + log("Hierarchy dump for: " + message); dumpViewHierarchy(); - Assert.fail("http://go/tapl : " + getContextDescription() + message); + + Assert.fail(message); } private String getContextDescription() { @@ -333,6 +446,17 @@ public final class LauncherInstrumentation { private UiObject2 verifyContainerType(ContainerType containerType) { assertEquals("Unexpected display rotation", mExpectedRotation, mDevice.getDisplayRotation()); + + // b/136278866 + for (int i = 0; i != 100; ++i) { + if (getNavigationModeMismatchError() == null) break; + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + final String error = getNavigationModeMismatchError(); assertTrue(error, error == null); log("verifyContainerType: " + containerType); @@ -614,12 +738,12 @@ public final class LauncherInstrumentation { @NonNull UiObject2 waitForLauncherObject(BySelector selector) { - return waitForObjectBySelector(selector.pkg(getLauncherPackageName())); + return waitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName())); } @NonNull UiObject2 tryWaitForLauncherObject(BySelector selector, long timeout) { - return tryWaitForObjectBySelector(selector.pkg(getLauncherPackageName()), timeout); + return tryWaitForObjectBySelector(By.copy(selector).pkg(getLauncherPackageName()), timeout); } @NonNull diff --git a/tests/tapl/com/android/launcher3/tapl/Widget.java b/tests/tapl/com/android/launcher3/tapl/Widget.java index 128789dbc1..1b6d8c4a6e 100644 --- a/tests/tapl/com/android/launcher3/tapl/Widget.java +++ b/tests/tapl/com/android/launcher3/tapl/Widget.java @@ -18,7 +18,16 @@ package com.android.launcher3.tapl; import androidx.test.uiautomator.UiObject2; -public class Widget { - Widget(LauncherInstrumentation launcher, UiObject2 widget) { +/** + * Widget in workspace or a widget list. + */ +public final class Widget extends Launchable { + Widget(LauncherInstrumentation launcher, UiObject2 icon) { + super(launcher, icon); + } + + @Override + protected String getLongPressIndicator() { + return "drop_target_bar"; } } diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java index 94003be919..2495933ac6 100644 --- a/tests/tapl/com/android/launcher3/tapl/Widgets.java +++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java @@ -16,6 +16,12 @@ package com.android.launcher3.tapl; +import static org.junit.Assert.fail; + +import android.graphics.Point; + +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.BySelector; import androidx.test.uiautomator.Direction; import androidx.test.uiautomator.UiObject2; @@ -75,4 +81,27 @@ public final class Widgets extends LauncherInstrumentation.VisibleContainer { protected LauncherInstrumentation.ContainerType getContainerType() { return LauncherInstrumentation.ContainerType.WIDGETS; } + + public Widget getWidget(String label) { + final int margin = ResourceUtils.getNavbarSize( + ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources()) + 1; + final UiObject2 widgetsContainer = verifyActiveContainer(); + widgetsContainer.setGestureMargins(0, 0, 0, margin); + + final Point displaySize = mLauncher.getRealDisplaySize(); + + int i = 0; + final BySelector selector = By. + clazz("com.android.launcher3.widget.WidgetCell"). + hasDescendant(By.text(label)); + + for (; ; ) { + final UiObject2 widget = mLauncher.tryWaitForLauncherObject(selector, 300); + if (widget != null && widget.getVisibleBounds().bottom <= displaySize.y - margin) { + return new Widget(mLauncher, widget); + } + if (++i > 40) fail("Too many attempts"); + widgetsContainer.scroll(Direction.DOWN, 1f); + } + } }