Merge "Preventing state change duing the swipe up animation when the previous app transition is not complete" into ub-launcher3-master
diff --git a/quickstep/res/layout/task_menu.xml b/quickstep/res/layout/task_menu.xml
index bf55ece..098b34f 100644
--- a/quickstep/res/layout/task_menu.xml
+++ b/quickstep/res/layout/task_menu.xml
@@ -24,11 +24,19 @@
     android:orientation="vertical"
     android:visibility="invisible">
 
+    <com.android.quickstep.views.IconView
+      android:id="@+id/task_icon"
+      android:layout_width="@dimen/task_thumbnail_icon_size"
+      android:layout_height="@dimen/task_thumbnail_icon_size"
+      android:layout_gravity="top|center_horizontal"
+      android:layout_marginBottom="@dimen/deep_shortcut_drawable_padding"
+      android:focusable="false"
+      android:importantForAccessibility="no" />
+
     <TextView
-        android:id="@+id/task_icon_and_name"
+        android:id="@+id/task_name"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:drawablePadding="@dimen/deep_shortcut_drawable_padding"
         android:gravity="center_horizontal"
         android:layout_marginBottom="16dp"
         android:textSize="12sp"/>
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 14633af..37d0b12 100644
--- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -537,6 +537,9 @@
             @Override
             public void onAnimationEnd(Animator animation) {
                 // Reset launcher to normal state
+                if (isBubbleTextView) {
+                    ((BubbleTextView) v).setStayPressed(false);
+                }
                 v.setVisibility(View.VISIBLE);
                 ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView);
             }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index ac9f863..2d0946b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -165,6 +165,14 @@
         }
     }
 
+    public static void onEnterAnimationComplete(Context context) {
+        // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled
+        // as a part of quickstep/scrub, so that high-res thumbnails can load the next time we
+        // enter overview
+        RecentsModel.getInstance(context).getRecentsTaskLoader()
+                .getHighResThumbnailLoader().setVisible(true);
+    }
+
     public static void onLauncherStateOrResumeChanged(Launcher launcher) {
         LauncherState state = launcher.getStateManager().getState();
         DeviceProfile profile = launcher.getDeviceProfile();
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index d37ac49..bb5bf6e 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -57,6 +57,7 @@
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.uioverrides.FastOverviewState;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -230,6 +231,8 @@
 
                 // Optimization, hide the all apps view to prevent layout while initializing
                 activity.getAppsView().getContentView().setVisibility(View.GONE);
+
+                AccessibilityManagerCompat.sendEventToTest(activity, "TAPL_WENT_TO_STATE");
             }
 
             return new AnimationFactory() {
@@ -299,6 +302,9 @@
         private void playScaleDownAnim(AnimatorSet anim, Launcher launcher) {
             RecentsView recentsView = launcher.getOverviewPanel();
             TaskView v = recentsView.getTaskViewAt(recentsView.getCurrentPage());
+            if (v == null) {
+                return;
+            }
             ClipAnimationHelper clipHelper = new ClipAnimationHelper();
             clipHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(), null);
             if (!clipHelper.getSourceRect().isEmpty() && !clipHelper.getTargetRect().isEmpty()) {
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 32079bf..b93a54b 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -232,6 +232,12 @@
     }
 
     @Override
+    public void onEnterAnimationComplete() {
+        super.onEnterAnimationComplete();
+        UiFactory.onEnterAnimationComplete(this);
+    }
+
+    @Override
     public void onTrimMemory(int level) {
         super.onTrimMemory(level);
         UiFactory.onTrimMemory(this, level);
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 0c8e47f..0b97f01 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -232,7 +232,6 @@
 
     public void onStart() {
         mRecentsTaskLoader.startLoader(mContext);
-        mRecentsTaskLoader.getHighResThumbnailLoader().setVisible(true);
     }
 
     public void onTrimMemory(int level) {
diff --git a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
index d9da002..24e199b 100644
--- a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
+++ b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
@@ -30,7 +30,6 @@
 import android.util.Log;
 import android.view.View;
 
-import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.ItemInfo;
@@ -270,9 +269,4 @@
             return null;
         }
     }
-
-    private static void dismissTaskMenuView(BaseDraggingActivity activity) {
-        AbstractFloatingView.closeOpenViews(activity, true,
-                AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 6491aec..95dcf48 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -486,6 +486,12 @@
         if (LatencyTrackerCompat.isEnabled(mContext)) {
             LatencyTrackerCompat.logToggleRecents((int) (mLauncherFrameDrawnTime - mTouchTimeMs));
         }
+
+        // This method is only called when STATE_GESTURE_STARTED_QUICKSTEP/
+        // STATE_GESTURE_STARTED_QUICKSCRUB is set, so we can enable the high-res thumbnail loader
+        // here once we are sure that we will end up in an overview state
+        RecentsModel.getInstance(mContext).getRecentsTaskLoader()
+                .getHighResThumbnailLoader().setVisible(true);
     }
 
     public void updateInteractionType(@InteractionType int interactionType) {
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
index c359966..8659949 100644
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ b/quickstep/src/com/android/quickstep/views/IconView.java
@@ -18,8 +18,11 @@
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
 import android.util.AttributeSet;
 import android.view.View;
+import com.android.launcher3.FastBitmapDrawable;
+import java.util.ArrayList;
 
 /**
  * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
@@ -27,8 +30,14 @@
  */
 public class IconView extends View {
 
+    public interface OnScaleUpdateListener {
+        public void onScaleUpdate(float scale);
+    }
+
     private Drawable mDrawable;
 
+    private ArrayList<OnScaleUpdateListener> mScaleListeners;
+
     public IconView(Context context) {
         super(context);
     }
@@ -53,6 +62,10 @@
         invalidate();
     }
 
+    public Drawable getDrawable() {
+        return mDrawable;
+    }
+
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
@@ -78,6 +91,16 @@
     }
 
     @Override
+    public void invalidateDrawable(@NonNull Drawable drawable) {
+        super.invalidateDrawable(drawable);
+        if (drawable instanceof FastBitmapDrawable && mScaleListeners != null) {
+            for (OnScaleUpdateListener listener : mScaleListeners) {
+                listener.onScaleUpdate(((FastBitmapDrawable) drawable).getScale());
+            }
+        }
+    }
+
+    @Override
     protected void onDraw(Canvas canvas) {
         if (mDrawable != null) {
             mDrawable.draw(canvas);
@@ -88,4 +111,20 @@
     public boolean hasOverlappingRendering() {
         return false;
     }
+
+    public void addUpdateScaleListener(OnScaleUpdateListener listener) {
+        if (mScaleListeners == null) {
+            mScaleListeners = new ArrayList<>();
+        }
+        mScaleListeners.add(listener);
+        if (mDrawable instanceof FastBitmapDrawable) {
+            listener.onScaleUpdate(((FastBitmapDrawable) mDrawable).getScale());
+        }
+    }
+
+    public void removeUpdateScaleListener(OnScaleUpdateListener listener) {
+        if (mScaleListeners != null) {
+            mScaleListeners.remove(listener);
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 098349a..41626c6 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -22,7 +22,6 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.Context;
-import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
@@ -34,15 +33,16 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.TaskSystemShortcut;
 import com.android.quickstep.TaskUtils;
+import com.android.quickstep.views.IconView.OnScaleUpdateListener;
 
 /**
  * Contains options for a recent task when long-pressing its icon.
@@ -59,14 +59,42 @@
             new TaskSystemShortcut.Install(),
     };
 
+    private final OnScaleUpdateListener mTaskViewIconScaleListener = new OnScaleUpdateListener() {
+        @Override
+        public void onScaleUpdate(float scale) {
+            final Drawable drawable = mTaskIcon.getDrawable();
+            if (drawable instanceof FastBitmapDrawable) {
+                if (scale != ((FastBitmapDrawable) drawable).getScale()) {
+                    mMenuIconDrawable.setScale(scale);
+                }
+            }
+        }
+    };
+
+    private final OnScaleUpdateListener mMenuIconScaleListener = new OnScaleUpdateListener() {
+        @Override
+        public void onScaleUpdate(float scale) {
+            final Drawable taskViewDrawable = mTaskView.getIconView().getDrawable();
+            if (taskViewDrawable instanceof FastBitmapDrawable) {
+                final float currentScale = ((FastBitmapDrawable) taskViewDrawable).getScale();
+                if (currentScale != scale) {
+                    ((FastBitmapDrawable) taskViewDrawable).setScale(scale);
+                }
+            }
+        }
+    };
+
     private static final int REVEAL_OPEN_DURATION = 150;
     private static final int REVEAL_CLOSE_DURATION = 100;
 
+    private final float mThumbnailTopMargin;
     private BaseDraggingActivity mActivity;
-    private TextView mTaskIconAndName;
+    private TextView mTaskName;
+    private IconView mTaskIcon;
     private AnimatorSet mOpenCloseAnimator;
     private TaskView mTaskView;
     private LinearLayout mOptionLayout;
+    private FastBitmapDrawable mMenuIconDrawable;
 
     public TaskMenuView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -76,12 +104,14 @@
         super(context, attrs, defStyleAttr);
 
         mActivity = BaseDraggingActivity.fromContext(context);
+        mThumbnailTopMargin = getResources().getDimension(R.dimen.task_thumbnail_top_margin);
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mTaskIconAndName = findViewById(R.id.task_icon_and_name);
+        mTaskName = findViewById(R.id.task_name);
+        mTaskIcon = findViewById(R.id.task_icon);
         mOptionLayout = findViewById(R.id.menu_option_layout);
     }
 
@@ -113,15 +143,29 @@
     }
 
     @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        // Remove all scale listeners when menu is removed
+        mTaskView.getIconView().removeUpdateScaleListener(mTaskViewIconScaleListener);
+        mTaskIcon.removeUpdateScaleListener(mMenuIconScaleListener);
+    }
+
+    @Override
     protected boolean isOfType(int type) {
         return (type & TYPE_TASK_MENU) != 0;
     }
 
-    public static boolean showForTask(TaskView taskView) {
+    public void setPosition(float x, float y) {
+        setX(x);
+        setY(y + mThumbnailTopMargin);
+    }
+
+    public static TaskMenuView showForTask(TaskView taskView) {
         BaseDraggingActivity activity = BaseDraggingActivity.fromContext(taskView.getContext());
         final TaskMenuView taskMenuView = (TaskMenuView) activity.getLayoutInflater().inflate(
                         R.layout.task_menu, activity.getDragLayer(), false);
-        return taskMenuView.populateAndShowForTask(taskView);
+        return taskMenuView.populateAndShowForTask(taskView) ? taskMenuView : null;
     }
 
     private boolean populateAndShowForTask(TaskView taskView) {
@@ -138,17 +182,21 @@
 
     private void addMenuOptions(TaskView taskView) {
         Drawable icon = taskView.getTask().icon.getConstantState().newDrawable();
-        int iconSize = getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_size);
-        icon.setBounds(0, 0, iconSize, iconSize);
-        mTaskIconAndName.setCompoundDrawables(null, icon, null, null);
-        mTaskIconAndName.setText(TaskUtils.getTitle(getContext(), taskView.getTask()));
-        mTaskIconAndName.setOnClickListener(v -> close(true));
+        mTaskIcon.setDrawable(icon);
+        mTaskIcon.setOnClickListener(v -> close(true));
+        mTaskName.setText(TaskUtils.getTitle(getContext(), taskView.getTask()));
+        mTaskName.setOnClickListener(v -> close(true));
+
+        // Set the icons to match scale by listening to each other's changes
+        mMenuIconDrawable = icon instanceof FastBitmapDrawable ? (FastBitmapDrawable) icon : null;
+        taskView.getIconView().addUpdateScaleListener(mTaskViewIconScaleListener);
+        mTaskIcon.addUpdateScaleListener(mMenuIconScaleListener);
 
         // Move the icon and text up half an icon size to lay over the TaskView
         LinearLayout.LayoutParams params =
-                (LinearLayout.LayoutParams) mTaskIconAndName.getLayoutParams();
-        params.topMargin = (int) -getResources().getDimension(R.dimen.task_thumbnail_top_margin);
-        mTaskIconAndName.setLayoutParams(params);
+                (LinearLayout.LayoutParams) mTaskIcon.getLayoutParams();
+        params.topMargin = (int) -mThumbnailTopMargin;
+        mTaskIcon.setLayoutParams(params);
 
         for (TaskSystemShortcut menuOption : MENU_OPTIONS) {
             OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, taskView);
@@ -172,12 +220,12 @@
         mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect);
         Rect insets = mActivity.getDragLayer().getInsets();
         BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
-        params.width = sTempRect.width();
-        params.gravity = Gravity.LEFT;
+        params.width = taskView.getMeasuredWidth();
+        params.gravity = Gravity.START;
         setLayoutParams(params);
-        setX(Math.round(sTempRect.left - insets.left));
-        setY(Math.round(sTempRect.top - insets.top
-                + getResources().getDimension(R.dimen.task_thumbnail_top_margin)));
+        setScaleX(taskView.getScaleX());
+        setScaleY(taskView.getScaleY());
+        setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top);
     }
 
     private void animateOpen() {
@@ -191,7 +239,7 @@
 
     private void animateOpenOrClosed(boolean closing) {
         if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
-            return;
+            mOpenCloseAnimator.end();
         }
         mOpenCloseAnimator = LauncherAnimUtils.createAnimatorSet();
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 1653038..ee542d5 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -110,8 +110,24 @@
         }
     };
 
+    private final OnAttachStateChangeListener mTaskMenuStateListener =
+            new OnAttachStateChangeListener() {
+                @Override
+                public void onViewAttachedToWindow(View view) {
+                }
+
+                @Override
+                public void onViewDetachedFromWindow(View view) {
+                    if (mMenuView != null) {
+                        mMenuView.removeOnAttachStateChangeListener(this);
+                        mMenuView = null;
+                    }
+                }
+            };
+
     private Task mTask;
     private TaskThumbnailView mSnapshotView;
+    private TaskMenuView mMenuView;
     private IconView mIconView;
     private float mCurveScale;
     private float mZoomScale;
@@ -200,13 +216,22 @@
     public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) {
         mSnapshotView.setThumbnail(task, thumbnailData);
         mIconView.setDrawable(task.icon);
-        mIconView.setOnClickListener(icon -> TaskMenuView.showForTask(this));
+        mIconView.setOnClickListener(icon -> showTaskMenu());
         mIconView.setOnLongClickListener(icon -> {
             requestDisallowInterceptTouchEvent(true);
-            return TaskMenuView.showForTask(this);
+            return showTaskMenu();
         });
     }
 
+    private boolean showTaskMenu() {
+        getRecentsView().snapToPage(getRecentsView().indexOfChild(this));
+        mMenuView = TaskMenuView.showForTask(this);
+        if (mMenuView != null) {
+            mMenuView.addOnAttachStateChangeListener(mTaskMenuStateListener);
+        }
+        return mMenuView != null;
+    }
+
     @Override
     public void onTaskDataUnloaded() {
         mSnapshotView.setThumbnail(null, null);
@@ -266,6 +291,12 @@
 
         mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA);
         setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation));
+
+        if (mMenuView != null) {
+            mMenuView.setPosition(getX() - getRecentsView().getScrollX(), getY());
+            mMenuView.setScaleX(getScaleX());
+            mMenuView.setScaleY(getScaleY());
+        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index 9217ca9..7efb6ec 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -109,7 +109,7 @@
 
     @Override
     public final void draw(Canvas canvas) {
-        if (mScaleAnimation != null) {
+        if (mScale != 1f) {
             int count = canvas.save();
             Rect bounds = getBounds();
             canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY());
@@ -150,10 +150,23 @@
         return mAlpha;
     }
 
+    public void setScale(float scale) {
+        if (mScaleAnimation != null) {
+            mScaleAnimation.cancel();
+            mScaleAnimation = null;
+        }
+        mScale = scale;
+        invalidateSelf();
+    }
+
     public float getAnimatedScale() {
         return mScaleAnimation == null ? 1 : mScale;
     }
 
+    public float getScale() {
+        return mScale;
+    }
+
     @Override
     public int getIntrinsicWidth() {
         return mBitmap.getWidth();
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 7ab9768..44d3d53 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import static android.content.pm.ActivityInfo.CONFIG_LOCALE;
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
 
@@ -85,6 +86,7 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.folder.FolderIconPreviewVerifier;
 import com.android.launcher3.keyboard.CustomActionsPopup;
@@ -347,8 +349,19 @@
     }
 
     @Override
+    public void onEnterAnimationComplete() {
+        super.onEnterAnimationComplete();
+        UiFactory.onEnterAnimationComplete(this);
+    }
+
+    @Override
     public void onConfigurationChanged(Configuration newConfig) {
         int diff = newConfig.diff(mOldConfig);
+
+        if ((diff & CONFIG_LOCALE) != 0) {
+            Folder.setLocaleDependentFields(getResources(), true /* force */);
+        }
+
         if ((diff & (CONFIG_ORIENTATION | CONFIG_SCREEN_SIZE)) != 0) {
             mUserEventDispatcher = null;
             initDeviceProfile(mDeviceProfile.inv);
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 8683b21..7c5bb1a 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import android.app.ActivityManager;
 import android.app.WallpaperManager;
 import android.content.ComponentName;
 import android.content.Context;
@@ -130,6 +131,9 @@
             CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
             TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
 
+    public static final boolean IS_RUNNING_IN_TEST_HARNESS =
+                    ActivityManager.isRunningInTestHarness();
+
     public static boolean isPropertyEnabled(String propertyName) {
         return Log.isLoggable(propertyName, Log.VERBOSE);
     }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 67bdd3b..37d0056 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -286,7 +286,9 @@
         mInsets.set(insets);
 
         DeviceProfile grid = mLauncher.getDeviceProfile();
-        mMaxDistanceForFolderCreation = (0.55f * grid.iconSizePx);
+        mMaxDistanceForFolderCreation = grid.isTablet
+                ? 0.75f * grid.iconSizePx
+                : 0.55f * grid.iconSizePx;
         mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
 
         Rect padding = grid.workspacePadding;
@@ -1034,7 +1036,7 @@
     }
 
     protected void onScrollInteractionBegin() {
-        super.onScrollInteractionEnd();
+        super.onScrollInteractionBegin();
         mScrollInteractionBegan = true;
     }
 
@@ -2630,16 +2632,10 @@
             case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
             case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
             case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
-                if (info.container == NO_ID) {
+                if (info.container == NO_ID && info instanceof AppInfo) {
                     // Came from all apps -- make a copy
-                    if (info instanceof AppInfo) {
-                        info = ((AppInfo) info).makeShortcut();
-                        d.dragInfo = info;
-                    } else if (info instanceof ShortcutInfo) {
-                        info = new ShortcutInfo((ShortcutInfo) info);
-                        d.dragInfo = info;
-                    }
-
+                    info = ((AppInfo) info).makeShortcut();
+                    d.dragInfo = info;
                 }
                 view = mLauncher.createShortcut(cellLayout, (ShortcutInfo) info);
                 break;
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index 0c78381..29fc2bc 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -16,11 +16,14 @@
 
 package com.android.launcher3.compat;
 
+import android.accessibilityservice.AccessibilityServiceInfo;
 import android.content.Context;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 
+import com.android.launcher3.Utilities;
+
 public class AccessibilityManagerCompat {
 
     public static boolean isAccessibilityEnabled(Context context) {
@@ -44,4 +47,19 @@
     private static AccessibilityManager getManager(Context context) {
         return (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
     }
+
+    public static void sendEventToTest(Context context, String eventTag) {
+        if (!Utilities.IS_RUNNING_IN_TEST_HARNESS) return;
+
+        final AccessibilityManager accessibilityManager = getManager(context);
+        if (accessibilityManager.isEnabled() &&
+                accessibilityManager.getEnabledAccessibilityServiceList(
+                        AccessibilityServiceInfo.FEEDBACK_ALL_MASK).size() == 0) {
+
+            final AccessibilityEvent e = AccessibilityEvent.obtain(
+                    AccessibilityEvent.TYPE_ANNOUNCEMENT);
+            e.setClassName(eventTag);
+            accessibilityManager.sendAccessibilityEvent(e);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 8a216fc..47bbbcb 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.dragndrop;
 
+import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
 import static com.android.launcher3.LauncherState.NORMAL;
 
@@ -31,6 +32,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.ItemInfo;
@@ -145,6 +147,7 @@
 
         // Hide soft keyboard, if visible
         UiThreadHelper.hideKeyboardAsync(mLauncher, mWindowToken);
+        AbstractFloatingView.closeOpenViews(mLauncher, false, TYPE_DISCOVERY_BOUNCE);
 
         mOptions = options;
         if (mOptions.systemDndStartPoint != null) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 6b13da7..6a3ebcf 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -25,6 +25,7 @@
 import android.animation.AnimatorSet;
 import android.annotation.SuppressLint;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.text.InputType;
@@ -191,14 +192,9 @@
     public Folder(Context context, AttributeSet attrs) {
         super(context, attrs);
         setAlwaysDrawnWithCacheEnabled(false);
-        Resources res = getResources();
 
-        if (sDefaultFolderName == null) {
-            sDefaultFolderName = res.getString(R.string.folder_name);
-        }
-        if (sHintText == null) {
-            sHintText = res.getString(R.string.folder_hint_text);
-        }
+        setLocaleDependentFields(getResources(), false /* force */);
+
         mLauncher = Launcher.getLauncher(context);
         // We need this view to be focusable in touch mode so that when text editing of the folder
         // name is complete, we have something to focus on, thus hiding the cursor and giving
@@ -1473,4 +1469,13 @@
         }
         return false;
     }
+
+    public static void setLocaleDependentFields(Resources res, boolean force) {
+        if (sDefaultFolderName == null || force) {
+            sDefaultFolderName = res.getString(R.string.folder_name);
+        }
+        if (sHintText == null || force) {
+            sHintText = res.getString(R.string.folder_hint_text);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index cb5d872..95a6bbd 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -77,7 +77,6 @@
     @Thunk Launcher mLauncher;
     @Thunk Folder mFolder;
     private FolderInfo mInfo;
-    @Thunk static boolean sStaticValuesDirty = true;
 
     private CheckLongPressHelper mLongPressHelper;
     private StylusEventHelper mStylusEventHelper;
@@ -185,12 +184,6 @@
         return icon;
     }
 
-    @Override
-    protected Parcelable onSaveInstanceState() {
-        sStaticValuesDirty = true;
-        return super.onSaveInstanceState();
-    }
-
     public Folder getFolder() {
         return mFolder;
     }
diff --git a/src/com/android/launcher3/graphics/NinePatchDrawHelper.java b/src/com/android/launcher3/graphics/NinePatchDrawHelper.java
index fc20926..5872689 100644
--- a/src/com/android/launcher3/graphics/NinePatchDrawHelper.java
+++ b/src/com/android/launcher3/graphics/NinePatchDrawHelper.java
@@ -33,7 +33,9 @@
 
     private final Rect mSrc = new Rect();
     private final RectF mDst = new RectF();
-    public final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    // Enable filtering to always get a nice edge. This avoids jagged line, when bitmap is
+    // translated by half pixel.
+    public final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
 
     /**
      * Draws the bitmap split into three parts horizontally, with the middle part having width
diff --git a/src/com/android/launcher3/graphics/ShadowGenerator.java b/src/com/android/launcher3/graphics/ShadowGenerator.java
index 5fbf502..88da853 100644
--- a/src/com/android/launcher3/graphics/ShadowGenerator.java
+++ b/src/com/android/launcher3/graphics/ShadowGenerator.java
@@ -126,13 +126,13 @@
         }
 
         public Bitmap createPill(int width, int height) {
-            radius = height / 2;
+            radius = height / 2f;
 
-            int centerX = Math.round(width / 2 + shadowBlur);
+            int centerX = Math.round(width / 2f + shadowBlur);
             int centerY = Math.round(radius + shadowBlur + keyShadowDistance);
             int center = Math.max(centerX, centerY);
             bounds.set(0, 0, width, height);
-            bounds.offsetTo(center - width / 2, center - height / 2);
+            bounds.offsetTo(center - width / 2f, center - height / 2f);
 
             int size = center * 2;
             Bitmap result = Bitmap.createBitmap(size, size, Config.ARGB_8888);
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 3c1cc90..693e532 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -75,6 +75,7 @@
         public View.OnClickListener getOnClickListener(
                 BaseDraggingActivity activity, ItemInfo itemInfo) {
             return (view) -> {
+                dismissTaskMenuView(activity);
                 Rect sourceBounds = activity.getViewBounds(view);
                 Bundle opts = activity.getActivityLaunchOptionsAsBundle(view);
                 new PackageManagerHelper(activity).startDetailsActivityForInfo(
@@ -117,4 +118,9 @@
             };
         }
     }
+
+    protected static void dismissTaskMenuView(BaseDraggingActivity activity) {
+        AbstractFloatingView.closeOpenViews(activity, true,
+            AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
+    }
 }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 55f850c..0e277ea 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -43,6 +43,7 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -515,6 +516,8 @@
                 logReachedState(logAction, targetState);
             }
             mLauncher.getStateManager().goToState(targetState, false /* animated */);
+
+            AccessibilityManagerCompat.sendEventToTest(mLauncher, "TAPL_WENT_TO_STATE");
         }
     }
 
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 5046639..dc6d2ff 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.support.annotation.VisibleForTesting;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.AttributeSet;
@@ -133,6 +134,11 @@
         popup.reorderAndShow(popup.getChildCount());
     }
 
+    @VisibleForTesting
+    public static OptionsPopupView getOptionsPopup(Launcher launcher) {
+        return launcher.findViewById(R.id.deep_shortcuts_container);
+    }
+
     public static void showDefaultOptions(Launcher launcher, float x, float y) {
         float halfSize = launcher.getResources().getDimension(R.dimen.options_menu_thumb_size) / 2;
         if (x < 0 || y < 0) {
diff --git a/src/com/android/launcher3/widget/WidgetsFullSheet.java b/src/com/android/launcher3/widget/WidgetsFullSheet.java
index e94d81d..b493228 100644
--- a/src/com/android/launcher3/widget/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsFullSheet.java
@@ -20,6 +20,7 @@
 import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.graphics.Rect;
+import android.support.annotation.VisibleForTesting;
 import android.util.AttributeSet;
 import android.util.Pair;
 import android.view.LayoutInflater;
@@ -224,6 +225,11 @@
         return sheet;
     }
 
+    @VisibleForTesting
+    public static WidgetsRecyclerView getWidgetsView(Launcher launcher) {
+        return launcher.findViewById(R.id.widgets_list_view);
+    }
+
     @Override
     protected int getElementsRowCount() {
         return mAdapter.getItemCount();
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
index 5a7e50f..0d727fd 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
@@ -48,6 +48,8 @@
 
     public static void onStart(Launcher launcher) { }
 
+    public static void onEnterAnimationComplete(Context context) {}
+
     public static void onLauncherStateOrResumeChanged(Launcher launcher) { }
 
     public static void onTrimMemory(Launcher launcher, int level) { }
diff --git a/tests/Android.mk b/tests/Android.mk
index f6f02fe..8ca84f8 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -30,3 +30,16 @@
 LOCAL_INSTRUMENTATION_FOR := Launcher3
 
 include $(BUILD_PACKAGE)
+
+#
+# Build rule for Tapl library.
+#
+include $(CLEAR_VARS)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator
+
+LOCAL_SRC_FILES := $(call all-java-files-under, tapl)
+
+LOCAL_SDK_VERSION := current
+LOCAL_MODULE := ub-launcher-tapl
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index af8b15c..89ca7f3 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -31,7 +31,6 @@
                        android:resource="@xml/appwidget_no_config" />
         </receiver>
 
-
         <receiver
             android:name="com.android.launcher3.testcomponent.AppWdigetHidden"
             android:label="Hidden widget">
diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromHome.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
similarity index 73%
rename from tests/tapl/com/android/launcher3/tapl/AllAppsFromHome.java
rename to tests/tapl/com/android/launcher3/tapl/AllApps.java
index 02f8183..d849e2d 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromHome.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -16,36 +16,32 @@
 
 package com.android.launcher3.tapl;
 
+import static org.junit.Assert.assertTrue;
+
 import android.support.annotation.NonNull;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiObject2;
 
 /**
- * Operations on AllApps opened from Home.
+ * Operations on AllApps opened from Home. Also a parent for All Apps opened from Overview.
  */
-public final class AllAppsFromHome {
+public class AllApps extends LauncherInstrumentation.VisibleContainer {
     private static final int MAX_SCROLL_ATTEMPTS = 40;
     private static final int MIN_INTERACT_SIZE = 100;
     private static final int FLING_SPEED = 12000;
 
-    private final Launcher mLauncher;
     private final int mHeight;
 
-    AllAppsFromHome(Launcher launcher) {
-        mLauncher = launcher;
-        final UiObject2 allAppsContainer = assertState();
+    AllApps(LauncherInstrumentation launcher) {
+        super(launcher);
+        final UiObject2 allAppsContainer = verifyActiveContainer();
         mHeight = allAppsContainer.getVisibleBounds().height();
     }
 
-    /**
-     * Asserts that we are in all apps.
-     *
-     * @return All apps container.
-     */
-    @NonNull
-    private UiObject2 assertState() {
-        return mLauncher.assertState(Launcher.State.ALL_APPS);
+    @Override
+    protected LauncherInstrumentation.ContainerType getContainerType() {
+        return LauncherInstrumentation.ContainerType.ALL_APPS;
     }
 
     /**
@@ -57,41 +53,45 @@
      */
     @NonNull
     public AppIcon getAppIcon(String appName) {
-        final UiObject2 allAppsContainer = assertState();
+        final UiObject2 allAppsContainer = verifyActiveContainer();
         final BySelector appIconSelector = AppIcon.getAppIconSelector(appName);
         if (!allAppsContainer.hasObject(appIconSelector)) {
             scrollBackToBeginning();
             int attempts = 0;
             while (!allAppsContainer.hasObject(appIconSelector) &&
                     allAppsContainer.scroll(Direction.DOWN, 0.8f)) {
-                mLauncher.assertTrue("Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS,
+                assertTrue("Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS,
                         ++attempts <= MAX_SCROLL_ATTEMPTS);
-                assertState();
+                verifyActiveContainer();
             }
         }
-        assertState();
+        verifyActiveContainer();
 
         final UiObject2 appIcon = mLauncher.getObjectInContainer(allAppsContainer, appIconSelector);
         ensureIconVisible(appIcon, allAppsContainer);
         return new AppIcon(mLauncher, appIcon);
     }
 
+    protected int getBottomMarginForSwipeUp() {
+        return 5;
+    }
+
     private void scrollBackToBeginning() {
-        final UiObject2 allAppsContainer = assertState();
+        final UiObject2 allAppsContainer = verifyActiveContainer();
 
         int attempts = 0;
-        allAppsContainer.setGestureMargins(5, 500, 5, 5);
+        allAppsContainer.setGestureMargins(5, 600, 5, getBottomMarginForSwipeUp());
 
         while (allAppsContainer.scroll(Direction.UP, 0.5f)) {
             mLauncher.waitForIdle();
-            assertState();
+            verifyActiveContainer();
 
-            mLauncher.assertTrue("Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS,
+            assertTrue("Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS,
                     ++attempts <= MAX_SCROLL_ATTEMPTS);
         }
 
         mLauncher.waitForIdle();
-        assertState();
+        verifyActiveContainer();
     }
 
     private void ensureIconVisible(UiObject2 appIcon, UiObject2 allAppsContainer) {
@@ -102,7 +102,7 @@
             final float pct = Math.max(((float) (MIN_INTERACT_SIZE - appHeight)) / mHeight, 0.2f);
             allAppsContainer.scroll(Direction.DOWN, pct);
             mLauncher.waitForIdle();
-            assertState();
+            verifyActiveContainer();
         }
     }
 
@@ -110,22 +110,22 @@
      * Flings forward (down) and waits the fling's end.
      */
     public void flingForward() {
-        final UiObject2 allAppsContainer = assertState();
+        final UiObject2 allAppsContainer = verifyActiveContainer();
         // Start the gesture in the center to avoid starting at elements near the top.
         allAppsContainer.setGestureMargins(0, 0, 0, mHeight / 2);
         allAppsContainer.fling(Direction.DOWN, FLING_SPEED);
-        assertState();
+        verifyActiveContainer();
     }
 
     /**
      * Flings backward (up) and waits the fling's end.
      */
     public void flingBackward() {
-        final UiObject2 allAppsContainer = assertState();
+        final UiObject2 allAppsContainer = verifyActiveContainer();
         // Start the gesture in the center, for symmetry with forward.
         allAppsContainer.setGestureMargins(0, mHeight / 2, 0, 0);
         allAppsContainer.fling(Direction.UP, FLING_SPEED);
-        assertState();
+        verifyActiveContainer();
     }
 
     /**
@@ -137,6 +137,6 @@
     @Deprecated
     @NonNull
     public UiObject2 getObjectDeprecated() {
-        return assertState();
+        return verifyActiveContainer();
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
index cba7086..bc0dfc6 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllAppsFromOverview.java
@@ -22,24 +22,17 @@
 
 /**
  * Operations on AllApps opened from Overview.
- * Scroll gestures that are OK for {@link AllAppsFromHome} may close it, so they are not supported.
  */
-public final class AllAppsFromOverview {
-    private final Launcher mLauncher;
+public final class AllAppsFromOverview extends AllApps {
 
-    AllAppsFromOverview(Launcher launcher) {
-        mLauncher = launcher;
-        assertState();
+    AllAppsFromOverview(LauncherInstrumentation launcher) {
+        super(launcher);
+        verifyActiveContainer();
     }
 
-    /**
-     * Asserts that we are in all apps.
-     *
-     * @return All apps container.
-     */
-    @NonNull
-    private UiObject2 assertState() {
-        return mLauncher.assertState(Launcher.State.ALL_APPS);
+    @Override
+    protected int getBottomMarginForSwipeUp() {
+        return 600;
     }
 
     /**
@@ -49,13 +42,13 @@
      */
     @NonNull
     public Overview switchBackToOverview() {
-        final UiObject2 allAppsContainer = assertState();
+        final UiObject2 allAppsContainer = verifyActiveContainer();
         // Swipe from the search box to the bottom.
         final UiObject2 qsb = mLauncher.waitForObjectInContainer(
                 allAppsContainer, "search_container_all_apps");
         final Point start = qsb.getVisibleCenter();
         final int endY = (int) (mLauncher.getDevice().getDisplayHeight() * 0.6);
-        mLauncher.swipe(start.x, start.y, start.x, endY, (endY - start.y) / 100);  // 100 px/step
+        mLauncher.swipe(start.x, start.y, start.x, endY);
 
         return new Overview(mLauncher);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 73a74f2..721f7a8 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.tapl;
 
+import static org.junit.Assert.assertTrue;
+
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiObject2;
@@ -26,25 +28,25 @@
  * App icon, whether in all apps or in workspace/
  */
 public final class AppIcon {
-    private final Launcher mLauncher;
+    private final LauncherInstrumentation mLauncher;
     private final UiObject2 mIcon;
 
-    AppIcon(Launcher launcher, UiObject2 icon) {
+    AppIcon(LauncherInstrumentation launcher, UiObject2 icon) {
         mLauncher = launcher;
         mIcon = icon;
     }
 
     static BySelector getAppIconSelector(String appName) {
-        return By.clazz(TextView.class).text(appName).pkg(Launcher.LAUNCHER_PKG);
+        return By.clazz(TextView.class).text(appName).pkg(LauncherInstrumentation.LAUNCHER_PKG);
     }
 
     /**
      * Clicks the icon to launch its app.
      */
-    public void launch() {
-        mLauncher.assertTrue("Launching an app didn't open a new window: " + mIcon.getText(),
-                mIcon.clickAndWait(Until.newWindow(), Launcher.APP_LAUNCH_TIMEOUT_MS));
-        mLauncher.assertState(Launcher.State.BACKGROUND);
+    public Background launch() {
+        assertTrue("Launching an app didn't open a new window: " + mIcon.getText(),
+                mIcon.clickAndWait(Until.newWindow(), LauncherInstrumentation.WAIT_TIME_MS));
+        return new Background(mLauncher);
     }
 
     UiObject2 getIcon() {
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
new file mode 100644
index 0000000..1aef979
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -0,0 +1,32 @@
+/*
+ * 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.tapl;
+
+/**
+ * Operations on a state when Launcher is inactive because some other app is active.
+ */
+public final class Background extends Home {
+
+    Background(LauncherInstrumentation launcher) {
+        super(launcher);
+    }
+
+    @Override
+    protected LauncherInstrumentation.ContainerType getContainerType() {
+        return LauncherInstrumentation.ContainerType.BACKGROUND;
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index 0ec1a64..436e5ff 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -16,38 +16,22 @@
 
 package com.android.launcher3.tapl;
 
-import static junit.framework.TestCase.assertTrue;
-
-import android.graphics.Point;
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiObject2;
-import android.view.KeyEvent;
 
 /**
  * Operations on the home screen.
+ *
+ * Launcher can be invoked both when its activity is in the foreground and when it is in the
+ * background. This class is a parent of the two classes {@link Background} and {@link Workspace}
+ * that essentially represents these two activity states. Any gestures (e.g., switchToOverview) that
+ * can be performed in both of these states can be defined here.
  */
-public final class Home {
+public abstract class Home extends LauncherInstrumentation.VisibleContainer {
 
-    private final Launcher mLauncher;
-    private final UiObject2 mHotseat;
-    private final int ICON_DRAG_SPEED = 2000;
-
-    Home(Launcher launcher) {
-        mLauncher = launcher;
-        assertState();
-        mHotseat = launcher.waitForLauncherObject("hotseat");
-    }
-
-    /**
-     * Asserts that we are in home.
-     *
-     * @return Workspace.
-     */
-    @NonNull
-    private UiObject2 assertState() {
-        return mLauncher.assertState(Launcher.State.HOME);
+    protected Home(LauncherInstrumentation launcher) {
+        super(launcher);
+        verifyActiveContainer();
     }
 
     /**
@@ -57,131 +41,19 @@
      */
     @NonNull
     public Overview switchToOverview() {
-        assertState();
+        verifyActiveContainer();
         if (mLauncher.isSwipeUpEnabled()) {
             final int height = mLauncher.getDevice().getDisplayHeight();
             final UiObject2 navBar = mLauncher.getSystemUiObject("navigation_bar_frame");
 
-            // Swipe from nav bar to 2/3rd down the screen.
             mLauncher.swipe(
                     navBar.getVisibleBounds().centerX(), navBar.getVisibleBounds().centerY(),
-                    navBar.getVisibleBounds().centerX(), height * 2 / 3,
-                    (navBar.getVisibleBounds().centerY() - height * 2 / 3) / 100); // 100 px/step
+                    navBar.getVisibleBounds().centerX(), height - 300
+            );
         } else {
             mLauncher.getSystemUiObject("recent_apps").click();
         }
 
         return new Overview(mLauncher);
     }
-
-    /**
-     * Swipes up to All Apps.
-     *
-     * @return the App Apps object.
-     */
-    @NonNull
-    public AllAppsFromHome switchToAllApps() {
-        assertState();
-        if (mLauncher.isSwipeUpEnabled()) {
-            int midX = mLauncher.getDevice().getDisplayWidth() / 2;
-            int height = mLauncher.getDevice().getDisplayHeight();
-            // Swipe from 6/7ths down the screen to 1/7th down the screen.
-            mLauncher.swipe(
-                    midX,
-                    height * 6 / 7,
-                    midX,
-                    height / 7,
-                    (height * 2 / 3) / 100); // 100 px/step
-        } else {
-            // Swipe from the hotseat to near the top, e.g. 10% of the screen.
-            final UiObject2 hotseat = mHotseat;
-            final Point start = hotseat.getVisibleCenter();
-            final int endY = (int) (mLauncher.getDevice().getDisplayHeight() * 0.1f);
-            mLauncher.swipe(
-                    start.x,
-                    start.y,
-                    start.x,
-                    endY,
-                    (start.y - endY) / 100); // 100 px/step
-        }
-
-        return new AllAppsFromHome(mLauncher);
-    }
-
-    /**
-     * Returns an icon for the app, if currently visible.
-     *
-     * @param appName name of the app
-     * @return app icon, if found, null otherwise.
-     */
-    @Nullable
-    public AppIcon tryGetWorkspaceAppIcon(String appName) {
-        final UiObject2 workspace = assertState();
-        final UiObject2 icon = workspace.findObject(AppIcon.getAppIconSelector(appName));
-        return icon != null ? new AppIcon(mLauncher, icon) : null;
-    }
-
-    /**
-     * Ensures that workspace is scrollable. If it's not, drags an icon icons from hotseat to the
-     * second screen.
-     */
-    public void ensureWorkspaceIsScrollable() {
-        final UiObject2 workspace = assertState();
-        if (!isWorkspaceScrollable(workspace)) {
-            dragIconToNextScreen(getHotseatAppIcon("Messages"), workspace);
-        }
-        assertTrue("Home screen workspace didn't become scrollable",
-                isWorkspaceScrollable(workspace));
-    }
-
-    private boolean isWorkspaceScrollable(UiObject2 workspace) {
-        return workspace.isScrollable();
-    }
-
-    @NonNull
-    private AppIcon getHotseatAppIcon(String appName) {
-        return new AppIcon(mLauncher, mLauncher.getObjectInContainer(
-                mHotseat, AppIcon.getAppIconSelector(appName)));
-    }
-
-    private void dragIconToNextScreen(AppIcon app, UiObject2 workspace) {
-        final Point dest = new Point(
-                mLauncher.getDevice().getDisplayWidth(), workspace.getVisibleBounds().centerY());
-        app.getIcon().drag(dest, ICON_DRAG_SPEED);
-        assertState();
-    }
-
-    /**
-     * Flings to get to screens on the right. Waits for scrolling and a possible overscroll
-     * recoil to complete.
-     */
-    public void flingForward() {
-        final UiObject2 workspace = assertState();
-        workspace.fling(Direction.RIGHT);
-        mLauncher.waitForIdle();
-        assertState();
-    }
-
-    /**
-     * Flings to get to screens on the left.  Waits for scrolling and a possible overscroll
-     * recoil to complete.
-     */
-    public void flingBackward() {
-        final UiObject2 workspace = assertState();
-        workspace.fling(Direction.LEFT);
-        mLauncher.waitForIdle();
-        assertState();
-    }
-
-    /**
-     * Opens widgets container by pressing Ctrl+W.
-     *
-     * @return the widgets container.
-     */
-    @NonNull
-    public Widgets openAllWidgets() {
-        assertState();
-        mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
-        return new Widgets(mLauncher);
-    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Launcher.java b/tests/tapl/com/android/launcher3/tapl/Launcher.java
deleted file mode 100644
index 5201dc8..0000000
--- a/tests/tapl/com/android/launcher3/tapl/Launcher.java
+++ /dev/null
@@ -1,303 +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.tapl;
-
-import static com.android.systemui.shared.system.SettingsCompat.SWIPE_UP_SETTING_NAME;
-
-import android.content.res.Resources;
-import android.os.RemoteException;
-import android.provider.Settings;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.BySelector;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
-import android.util.Log;
-
-import org.junit.Assert;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
-/**
- * The main tapl object. The only object that can be explicitly constructed by the using code. It
- * produces all other objects.
- */
-public final class Launcher {
-
-    private static final String WORKSPACE_RES_ID = "workspace";
-    private static final String APPS_RES_ID = "apps_view";
-    private static final String OVERVIEW_RES_ID = "overview_panel";
-    private static final String WIDGETS_RES_ID = "widgets_list_view";
-
-    enum State {HOME, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND}
-
-    static final String LAUNCHER_PKG = "com.google.android.apps.nexuslauncher";
-    static final int APP_LAUNCH_TIMEOUT_MS = 10000;
-    private static final int UI_OBJECT_WAIT_TIMEOUT_MS = 10000;
-    private static final String SWIPE_UP_SETTING_AVAILABLE_RES_NAME =
-            "config_swipe_up_gesture_setting_available";
-    private static final String SWIPE_UP_ENABLED_DEFAULT_RES_NAME =
-            "config_swipe_up_gesture_default";
-    private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
-    private static final String TAG = "tapl.Launcher";
-    private final UiDevice mDevice;
-    private final boolean mSwipeUpEnabled;
-
-    /**
-     * Constructs the root of TAPL hierarchy. You get all other object from it.
-     */
-    public Launcher(UiDevice device) {
-        mDevice = device;
-        final boolean swipeUpEnabledDefault =
-                !getSystemBooleanRes(SWIPE_UP_SETTING_AVAILABLE_RES_NAME) ||
-                        getSystemBooleanRes(SWIPE_UP_ENABLED_DEFAULT_RES_NAME);
-        mSwipeUpEnabled = Settings.Secure.getInt(
-                InstrumentationRegistry.getTargetContext().getContentResolver(),
-                SWIPE_UP_SETTING_NAME,
-                swipeUpEnabledDefault ? 1 : 0) == 1;
-    }
-
-    private boolean getSystemBooleanRes(String resName) {
-        final Resources res = Resources.getSystem();
-        final int resId = res.getIdentifier(resName, "bool", "android");
-        assertTrue("Resource not found: " + resName, resId != 0);
-        return res.getBoolean(resId);
-    }
-
-    private void dumpViewHierarchy() {
-        final ByteArrayOutputStream stream = new ByteArrayOutputStream();
-        try {
-            mDevice.dumpWindowHierarchy(stream);
-            stream.flush();
-            stream.close();
-            for (String line : stream.toString().split("\\r?\\n")) {
-                Log.e(TAG, line.trim());
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "error dumping XML to logcat", e);
-        }
-    }
-
-    void fail(String message) {
-        dumpViewHierarchy();
-        Assert.fail(message);
-    }
-
-    void assertTrue(String message, boolean condition) {
-        if (!condition) {
-            fail(message);
-        }
-    }
-
-    void assertNotNull(String message, Object object) {
-        assertTrue(message, object != null);
-    }
-
-    private void failEquals(String message, Object actual) {
-        String formatted = "Values should be different. ";
-        if (message != null) {
-            formatted = message + ". ";
-        }
-
-        formatted += "Actual: " + actual;
-        fail(formatted);
-    }
-
-    void assertNotEquals(String message, int unexpected, int actual) {
-        if (unexpected == actual) {
-            failEquals(message, actual);
-        }
-    }
-
-    boolean isSwipeUpEnabled() {
-        return mSwipeUpEnabled;
-    }
-
-    UiObject2 assertState(State state) {
-        switch (state) {
-            case HOME: {
-                //waitUntilGone(APPS_RES_ID);
-                waitUntilGone(OVERVIEW_RES_ID);
-                waitUntilGone(WIDGETS_RES_ID);
-                return waitForLauncherObject(WORKSPACE_RES_ID);
-            }
-            case WIDGETS: {
-                waitUntilGone(WORKSPACE_RES_ID);
-                waitUntilGone(APPS_RES_ID);
-                waitUntilGone(OVERVIEW_RES_ID);
-                return waitForLauncherObject(WIDGETS_RES_ID);
-            }
-            case ALL_APPS: {
-                waitUntilGone(OVERVIEW_RES_ID);
-                waitUntilGone(WORKSPACE_RES_ID);
-                waitUntilGone(WIDGETS_RES_ID);
-                return waitForLauncherObject(APPS_RES_ID);
-            }
-            case OVERVIEW: {
-                //waitForLauncherObject(APPS_RES_ID);
-                waitUntilGone(WORKSPACE_RES_ID);
-                waitUntilGone(WIDGETS_RES_ID);
-                return waitForLauncherObject(OVERVIEW_RES_ID);
-            }
-            case BACKGROUND: {
-                waitUntilGone(WORKSPACE_RES_ID);
-                waitUntilGone(APPS_RES_ID);
-                waitUntilGone(OVERVIEW_RES_ID);
-                waitUntilGone(WIDGETS_RES_ID);
-                return null;
-            }
-            default:
-                fail("Invalid state: " + state);
-                return null;
-        }
-    }
-
-    /**
-     * Presses nav bar home button.
-     *
-     * @return the Home object.
-     */
-    public Home pressHome() {
-        getSystemUiObject("home").click();
-        return getHome();
-    }
-
-    /**
-     * Gets the Home object if the current state is "active home", i.e. workspace. Fails if the
-     * launcher is not in that state.
-     *
-     * @return Home object.
-     */
-    @NonNull
-    public Home getHome() {
-        return new Home(this);
-    }
-
-    /**
-     * Gets the Widgets object if the current state is showing all widgets. Fails if the launcher is
-     * not in that state.
-     *
-     * @return Widgets object.
-     */
-    @NonNull
-    public Widgets getAllWidgets() {
-        return new Widgets(this);
-    }
-
-    /**
-     * Gets the Overview object if the current state is showing the overview panel. Fails if the
-     * launcher is not in that state.
-     *
-     * @return Overview object.
-     */
-    @NonNull
-    public Overview getOverview() {
-        return new Overview(this);
-    }
-
-    /**
-     * Gets the All Apps object if the current state is showing the all apps panel. Fails if the
-     * launcher is not in that state.
-     *
-     * @return All Aps object.
-     */
-    @NonNull
-    public AllAppsFromHome getAllApps() {
-        return new AllAppsFromHome(this);
-    }
-
-    /**
-     * Gets the All Apps object if the current state is showing the all apps panel. Returns null if
-     * the launcher is not in that state.
-     *
-     * @return All Aps object or null.
-     */
-    @Nullable
-    public AllAppsFromHome tryGetAllApps() {
-        return tryGetLauncherObject(APPS_RES_ID) != null ? getAllApps() : null;
-    }
-
-    private void waitUntilGone(String resId) {
-//        assertTrue("Unexpected launcher object visible: " + resId,
-//                mDevice.wait(Until.gone(getLauncherObjectSelector(resId)),
-//                        UI_OBJECT_WAIT_TIMEOUT_MS));
-    }
-
-    @NonNull
-    UiObject2 getSystemUiObject(String resId) {
-        try {
-            mDevice.wakeUp();
-        } catch (RemoteException e) {
-            fail("Failed to wake up the device: " + e);
-        }
-        final UiObject2 object = mDevice.findObject(By.res(SYSTEMUI_PACKAGE, resId));
-        assertNotNull("Can't find a systemui object with id: " + resId, object);
-        return object;
-    }
-
-    @NonNull
-    UiObject2 getObjectInContainer(UiObject2 container, BySelector selector) {
-        final UiObject2 object = container.findObject(selector);
-        assertNotNull("Can't find an object with selector: " + selector, object);
-        return object;
-    }
-
-    @Nullable
-    private UiObject2 tryGetLauncherObject(String resName) {
-        return mDevice.findObject(getLauncherObjectSelector(resName));
-    }
-
-    @NonNull
-    UiObject2 waitForObjectInContainer(UiObject2 container, String resName) {
-        final UiObject2 object = container.wait(
-                Until.findObject(getLauncherObjectSelector(resName)),
-                UI_OBJECT_WAIT_TIMEOUT_MS);
-        assertNotNull("Can find a launcher object id: " + resName + " in container: " +
-                container.getResourceName(), object);
-        return object;
-    }
-
-    @NonNull
-    UiObject2 waitForLauncherObject(String resName) {
-        final UiObject2 object = mDevice.wait(Until.findObject(getLauncherObjectSelector(resName)),
-                UI_OBJECT_WAIT_TIMEOUT_MS);
-        assertNotNull("Can find a launcher object; id: " + resName, object);
-        return object;
-    }
-
-    static BySelector getLauncherObjectSelector(String resName) {
-        return By.res(LAUNCHER_PKG, resName);
-    }
-
-    @NonNull
-    UiDevice getDevice() {
-        return mDevice;
-    }
-
-    void swipe(int startX, int startY, int endX, int endY, int steps) {
-        mDevice.swipe(startX, startY, endX, endY, steps);
-        waitForIdle();
-    }
-
-    void waitForIdle() {
-        mDevice.waitForIdle();
-    }
-}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
new file mode 100644
index 0000000..fc32fed
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -0,0 +1,336 @@
+/*
+ * 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.tapl;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.ActivityManager;
+import android.app.UiAutomation;
+import android.content.res.Resources;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import org.junit.Assert;
+
+import java.lang.ref.WeakReference;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * The main tapl object. The only object that can be explicitly constructed by the using code. It
+ * produces all other objects.
+ */
+public final class LauncherInstrumentation {
+
+    // Types for launcher containers that the user is interacting with. "Background" is a
+    // pseudo-container corresponding to inactive launcher covered by another app.
+    enum ContainerType {
+        WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND
+    }
+
+    // Base class for launcher containers.
+    static abstract class VisibleContainer {
+        protected final LauncherInstrumentation mLauncher;
+
+        protected VisibleContainer(LauncherInstrumentation launcher) {
+            mLauncher = launcher;
+            launcher.setActiveContainer(this);
+        }
+
+        protected abstract ContainerType getContainerType();
+
+        /**
+         * Asserts that the launcher is in the mode matching 'this' object.
+         *
+         * @return UI object for the container.
+         */
+        final UiObject2 verifyActiveContainer() {
+            assertTrue("Attempt to use a stale container", this == sActiveContainer.get());
+            return mLauncher.verifyContainerType(getContainerType());
+        }
+    }
+
+    private static final String WORKSPACE_RES_ID = "workspace";
+    private static final String APPS_RES_ID = "apps_view";
+    private static final String OVERVIEW_RES_ID = "overview_panel";
+    private static final String WIDGETS_RES_ID = "widgets_list_view";
+    static final String LAUNCHER_PKG = "com.google.android.apps.nexuslauncher";
+    static final int WAIT_TIME_MS = 10000;
+    private static final String SWIPE_UP_SETTING_AVAILABLE_RES_NAME =
+            "config_swipe_up_gesture_setting_available";
+    private static final String SWIPE_UP_ENABLED_DEFAULT_RES_NAME =
+            "config_swipe_up_gesture_default";
+    private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+
+    private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);
+
+    private final UiDevice mDevice;
+    private final boolean mSwipeUpEnabled;
+
+    /**
+     * Constructs the root of TAPL hierarchy. You get all other objects from it.
+     */
+    public LauncherInstrumentation(UiDevice device) {
+        mDevice = device;
+        final boolean swipeUpEnabledDefault =
+                !getSystemBooleanRes(SWIPE_UP_SETTING_AVAILABLE_RES_NAME) ||
+                        getSystemBooleanRes(SWIPE_UP_ENABLED_DEFAULT_RES_NAME);
+        mSwipeUpEnabled = Settings.Secure.getInt(
+                InstrumentationRegistry.getTargetContext().getContentResolver(),
+                "swipe_up_to_switch_apps_enabled",
+                swipeUpEnabledDefault ? 1 : 0) == 1;
+        assertTrue("Device must run in a test harness", ActivityManager.isRunningInTestHarness());
+    }
+
+    private boolean getSystemBooleanRes(String resName) {
+        final Resources res = Resources.getSystem();
+        final int resId = res.getIdentifier(resName, "bool", "android");
+        assertTrue("Resource not found: " + resName, resId != 0);
+        return res.getBoolean(resId);
+    }
+
+    void setActiveContainer(VisibleContainer container) {
+        sActiveContainer = new WeakReference<>(container);
+    }
+
+    boolean isSwipeUpEnabled() {
+        return mSwipeUpEnabled;
+    }
+
+    private UiObject2 verifyContainerType(ContainerType containerType) {
+        switch (containerType) {
+            case WORKSPACE: {
+                waitUntilGone(APPS_RES_ID);
+                waitUntilGone(OVERVIEW_RES_ID);
+                waitUntilGone(WIDGETS_RES_ID);
+                return waitForLauncherObject(WORKSPACE_RES_ID);
+            }
+            case WIDGETS: {
+                waitUntilGone(WORKSPACE_RES_ID);
+                waitUntilGone(APPS_RES_ID);
+                waitUntilGone(OVERVIEW_RES_ID);
+                return waitForLauncherObject(WIDGETS_RES_ID);
+            }
+            case ALL_APPS: {
+                waitUntilGone(OVERVIEW_RES_ID);
+                waitUntilGone(WIDGETS_RES_ID);
+                return waitForLauncherObject(APPS_RES_ID);
+            }
+            case OVERVIEW: {
+                waitForLauncherObject(APPS_RES_ID);
+                waitUntilGone(WIDGETS_RES_ID);
+                return waitForLauncherObject(OVERVIEW_RES_ID);
+            }
+            case BACKGROUND: {
+                waitUntilGone(WORKSPACE_RES_ID);
+                waitUntilGone(APPS_RES_ID);
+                waitUntilGone(OVERVIEW_RES_ID);
+                waitUntilGone(WIDGETS_RES_ID);
+                return null;
+            }
+            default:
+                fail("Invalid state: " + containerType);
+                return null;
+        }
+    }
+
+    private void executeAndWaitForEvent(Runnable command,
+            UiAutomation.AccessibilityEventFilter eventFilter, String message) {
+        try {
+            assertNotNull("executeAndWaitForEvent returned null (this can't happen)",
+                    InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                            .executeAndWaitForEvent(
+                                    command, eventFilter, WAIT_TIME_MS));
+        } catch (TimeoutException e) {
+            fail(message);
+        }
+    }
+
+    /**
+     * Presses nav bar home button.
+     *
+     * @return the Workspace object.
+     */
+    public Workspace pressHome() {
+        // Click home, then wait for any accessibility event, then wait until accessibility events
+        // stop.
+        // We need waiting for any accessibility event generated after pressing Home because
+        // otherwise waitForIdle may return immediately in case when there was a big enough pause in
+        // accessibility events prior to pressing Home.
+        executeAndWaitForEvent(
+                () -> getSystemUiObject("home").click(),
+                event -> true,
+                "Pressing Home didn't produce any events");
+        mDevice.waitForIdle();
+        return getWorkspace();
+    }
+
+    /**
+     * Gets the Workspace object if the current state is "active home", i.e. workspace. Fails if the
+     * launcher is not in that state.
+     *
+     * @return Workspace object.
+     */
+    @NonNull
+    public Workspace getWorkspace() {
+        return new Workspace(this);
+    }
+
+    /**
+     * Gets the Workspace object if the current state is "background home", i.e. some other app is
+     * active. Fails if the launcher is not in that state.
+     *
+     * @return Background object.
+     */
+    @NonNull
+    public Background getBackground() {
+        return new Background(this);
+    }
+
+    /**
+     * Gets the Widgets object if the current state is showing all widgets. Fails if the launcher is
+     * not in that state.
+     *
+     * @return Widgets object.
+     */
+    @NonNull
+    public Widgets getAllWidgets() {
+        return new Widgets(this);
+    }
+
+    /**
+     * Gets the Overview object if the current state is showing the overview panel. Fails if the
+     * launcher is not in that state.
+     *
+     * @return Overview object.
+     */
+    @NonNull
+    public Overview getOverview() {
+        return new Overview(this);
+    }
+
+    /**
+     * Gets the All Apps object if the current state is showing the all apps panel opened by swiping
+     * from workspace. Fails if the launcher is not in that state. Please don't call this method if
+     * App Apps was opened by swiping up from Overview, as it won't fail and will return an
+     * incorrect object.
+     *
+     * @return All Aps object.
+     */
+    @NonNull
+    public AllApps getAllApps() {
+        return new AllApps(this);
+    }
+
+    /**
+     * Gets the All Apps object if the current state is showing the all apps panel opened by swiping
+     * from overview. Fails if the launcher is not in that state. Please don't call this method if
+     * App Apps was opened by swiping up from home, as it won't fail and will return an
+     * incorrect object.
+     *
+     * @return All Aps object.
+     */
+    @NonNull
+    public AllAppsFromOverview getAllAppsFromOverview() {
+        return new AllAppsFromOverview(this);
+    }
+
+    /**
+     * Gets the All Apps object if the current state is showing the all apps panel opened by swiping
+     * from workspace. Returns null if launcher is not in that state. Please don't call this method
+     * if App Apps was opened by swiping up from Overview, as it won't fail and will return an
+     * incorrect object.
+     *
+     * @return All Aps object or null.
+     */
+    @Nullable
+    public AllApps tryGetAllApps() {
+        return tryGetLauncherObject(APPS_RES_ID) != null ? getAllApps() : null;
+    }
+
+    private void waitUntilGone(String resId) {
+        assertTrue("Unexpected launcher object visible: " + resId,
+                mDevice.wait(Until.gone(getLauncherObjectSelector(resId)),
+                        WAIT_TIME_MS));
+    }
+
+    @NonNull
+    UiObject2 getSystemUiObject(String resId) {
+        final UiObject2 object = mDevice.findObject(By.res(SYSTEMUI_PACKAGE, resId));
+        assertNotNull("Can't find a systemui object with id: " + resId, object);
+        return object;
+    }
+
+    @NonNull
+    UiObject2 getObjectInContainer(UiObject2 container, BySelector selector) {
+        final UiObject2 object = container.findObject(selector);
+        assertNotNull("Can't find an object with selector: " + selector, object);
+        return object;
+    }
+
+    @Nullable
+    private UiObject2 tryGetLauncherObject(String resName) {
+        return mDevice.findObject(getLauncherObjectSelector(resName));
+    }
+
+    @NonNull
+    UiObject2 waitForObjectInContainer(UiObject2 container, String resName) {
+        final UiObject2 object = container.wait(
+                Until.findObject(getLauncherObjectSelector(resName)),
+                WAIT_TIME_MS);
+        assertNotNull("Can't find a launcher object id: " + resName + " in container: " +
+                container.getResourceName(), object);
+        return object;
+    }
+
+    @NonNull
+    UiObject2 waitForLauncherObject(String resName) {
+        final UiObject2 object = mDevice.wait(Until.findObject(getLauncherObjectSelector(resName)),
+                WAIT_TIME_MS);
+        assertNotNull("Can't find a launcher object; id: " + resName, object);
+        return object;
+    }
+
+    static BySelector getLauncherObjectSelector(String resName) {
+        return By.res(LAUNCHER_PKG, resName);
+    }
+
+    @NonNull
+    UiDevice getDevice() {
+        return mDevice;
+    }
+
+    void swipe(int startX, int startY, int endX, int endY) {
+        executeAndWaitForEvent(
+                () -> mDevice.swipe(startX, startY, endX, endY, 60),
+                event -> "TAPL_WENT_TO_STATE".equals(event.getClassName()),
+                "Swipe failed to receive an event for the swipe end: " + startX + ", " + startY
+                        + ", " + endX + ", " + endY);
+    }
+
+    void waitForIdle() {
+        mDevice.waitForIdle();
+    }
+}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java
index 2251655..3d504c4 100644
--- a/tests/tapl/com/android/launcher3/tapl/Overview.java
+++ b/tests/tapl/com/android/launcher3/tapl/Overview.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.tapl;
 
+import static org.junit.Assert.assertNotEquals;
+
 import android.graphics.Point;
 import android.support.annotation.NonNull;
 import android.support.test.uiautomator.Direction;
@@ -27,44 +29,37 @@
 /**
  * Overview pane.
  */
-public final class Overview {
+public final class Overview extends LauncherInstrumentation.VisibleContainer {
     private static final int DEFAULT_FLING_SPEED = 15000;
 
-    private final Launcher mLauncher;
-
-    Overview(Launcher launcher) {
-        mLauncher = launcher;
-        assertState();
+    Overview(LauncherInstrumentation launcher) {
+        super(launcher);
+        verifyActiveContainer();
     }
 
-    /**
-     * Asserts that we are in overview.
-     *
-     * @return Overview panel.
-     */
-    @NonNull
-    private UiObject2 assertState() {
-        return mLauncher.assertState(Launcher.State.OVERVIEW);
+    @Override
+    protected LauncherInstrumentation.ContainerType getContainerType() {
+        return LauncherInstrumentation.ContainerType.OVERVIEW;
     }
 
     /**
      * Flings forward (left) and waits the fling's end.
      */
     public void flingForward() {
-        final UiObject2 overview = assertState();
+        final UiObject2 overview = verifyActiveContainer();
         overview.fling(Direction.LEFT, DEFAULT_FLING_SPEED);
         mLauncher.waitForIdle();
-        assertState();
+        verifyActiveContainer();
     }
 
     /**
      * Flings backward (right) and waits the fling's end.
      */
     public void flingBackward() {
-        final UiObject2 overview = assertState();
+        final UiObject2 overview = verifyActiveContainer();
         overview.fling(Direction.RIGHT, DEFAULT_FLING_SPEED);
         mLauncher.waitForIdle();
-        assertState();
+        verifyActiveContainer();
     }
 
     /**
@@ -74,10 +69,10 @@
      */
     @NonNull
     public OverviewTask getCurrentTask() {
-        assertState();
+        verifyActiveContainer();
         final List<UiObject2> taskViews = mLauncher.getDevice().findObjects(
-                Launcher.getLauncherObjectSelector("snapshot"));
-        mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
+                LauncherInstrumentation.getLauncherObjectSelector("snapshot"));
+        assertNotEquals("Unable to find a task", 0, taskViews.size());
 
         // taskViews contains up to 3 task views: the 'main' (having the widest visible
         // part) one in the center, and parts of its right and left siblings. Find the
@@ -86,7 +81,7 @@
                 (t1, t2) -> Integer.compare(t1.getVisibleBounds().width(),
                         t2.getVisibleBounds().width()));
 
-        return new OverviewTask(mLauncher, widestTask);
+        return new OverviewTask(mLauncher, widestTask, this);
     }
 
     /**
@@ -96,7 +91,7 @@
      */
     @NonNull
     public AllAppsFromOverview switchToAllApps() {
-        assertState();
+        verifyActiveContainer();
 
         // Swipe from the hotseat to near the top, e.g. 10% of the screen.
         final UiObject2 predictionRow = mLauncher.waitForLauncherObject(
@@ -104,7 +99,7 @@
         final Point start = predictionRow.getVisibleCenter();
         final int endY = (int) (mLauncher.getDevice().getDisplayHeight() * 0.1f);
         mLauncher.swipe(
-                start.x, start.y, start.x, endY, (start.y - endY) / 100); // 100 px/step
+                start.x, start.y, start.x, endY);
 
         return new AllAppsFromOverview(mLauncher);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 68d3082..0b3a264 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.tapl;
 
+import static org.junit.Assert.assertTrue;
+
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
@@ -24,29 +26,26 @@
  * A recent task in the overview panel carousel.
  */
 public final class OverviewTask {
-    private final Launcher mLauncher;
+    private final LauncherInstrumentation mLauncher;
     private final UiObject2 mTask;
+    private final Overview mOverview;
 
-    OverviewTask(Launcher launcher, UiObject2 task) {
+    OverviewTask(LauncherInstrumentation launcher, UiObject2 task, Overview overview) {
         mLauncher = launcher;
-        assertState();
         mTask = task;
+        mOverview = overview;
+        verifyActiveContainer();
     }
 
-    /**
-     * Asserts that we are in overview.
-     *
-     * @return Overview panel.
-     */
-    private void assertState() {
-        mLauncher.assertState(Launcher.State.OVERVIEW);
+    private void verifyActiveContainer() {
+        mOverview.verifyActiveContainer();
     }
 
     /**
      * Swipes the task up.
      */
     public void dismiss() {
-        assertState();
+        verifyActiveContainer();
         // Dismiss the task via flinging it up.
         mTask.fling(Direction.DOWN);
         mLauncher.waitForIdle();
@@ -55,11 +54,11 @@
     /**
      * Clicks at the task.
      */
-    public void open() {
-        assertState();
-        mLauncher.assertTrue("Launching task didn't open a new window: " +
+    public Background open() {
+        verifyActiveContainer();
+        assertTrue("Launching task didn't open a new window: " +
                         mTask.getParent().getContentDescription(),
-                mTask.clickAndWait(Until.newWindow(), Launcher.APP_LAUNCH_TIMEOUT_MS));
-        mLauncher.assertState(Launcher.State.BACKGROUND);
+                mTask.clickAndWait(Until.newWindow(), LauncherInstrumentation.WAIT_TIME_MS));
+        return new Background(mLauncher);
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 7a5198a..f67ef4c 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -16,50 +16,43 @@
 
 package com.android.launcher3.tapl;
 
-import android.support.annotation.NonNull;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiObject2;
 
 /**
  * All widgets container.
  */
-public final class Widgets {
+public final class Widgets extends LauncherInstrumentation.VisibleContainer {
     private static final int FLING_SPEED = 12000;
 
-    private final Launcher mLauncher;
-
-    Widgets(Launcher launcher) {
-        mLauncher = launcher;
-        assertState();
+    Widgets(LauncherInstrumentation launcher) {
+        super(launcher);
+        verifyActiveContainer();
     }
 
     /**
      * Flings forward (down) and waits the fling's end.
      */
     public void flingForward() {
-        final UiObject2 widgetsContainer = assertState();
+        final UiObject2 widgetsContainer = verifyActiveContainer();
+        widgetsContainer.setGestureMargin(100);
         widgetsContainer.fling(Direction.DOWN, FLING_SPEED);
-        mLauncher.waitForIdle();
-        assertState();
+        verifyActiveContainer();
     }
 
     /**
      * Flings backward (up) and waits the fling's end.
      */
     public void flingBackward() {
-        final UiObject2 widgetsContainer = assertState();
+        final UiObject2 widgetsContainer = verifyActiveContainer();
+        widgetsContainer.setGestureMargin(100);
         widgetsContainer.fling(Direction.UP, FLING_SPEED);
         mLauncher.waitForIdle();
-        assertState();
+        verifyActiveContainer();
     }
 
-    /**
-     * Asserts that we are in widgets.
-     *
-     * @return Widgets container.
-     */
-    @NonNull
-    private UiObject2 assertState() {
-        return mLauncher.assertState(Launcher.State.WIDGETS);
+    @Override
+    protected LauncherInstrumentation.ContainerType getContainerType() {
+        return LauncherInstrumentation.ContainerType.WIDGETS;
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
new file mode 100644
index 0000000..045ab5f
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -0,0 +1,155 @@
+/*
+ * 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.tapl;
+
+import static junit.framework.TestCase.assertTrue;
+
+import android.graphics.Point;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.view.KeyEvent;
+
+/**
+ * Operations on the workspace screen.
+ */
+public final class Workspace extends Home {
+    private final UiObject2 mHotseat;
+    private final int ICON_DRAG_SPEED = 2000;
+
+    Workspace(LauncherInstrumentation launcher) {
+        super(launcher);
+        mHotseat = launcher.waitForLauncherObject("hotseat");
+    }
+
+    @Override
+    protected LauncherInstrumentation.ContainerType getContainerType() {
+        return LauncherInstrumentation.ContainerType.WORKSPACE;
+    }
+
+    /**
+     * Swipes up to All Apps.
+     *
+     * @return the App Apps object.
+     */
+    @NonNull
+    public AllApps switchToAllApps() {
+        verifyActiveContainer();
+        if (mLauncher.isSwipeUpEnabled()) {
+            int midX = mLauncher.getDevice().getDisplayWidth() / 2;
+            int height = mLauncher.getDevice().getDisplayHeight();
+            // Swipe from 6/7ths down the screen to 1/7th down the screen.
+            mLauncher.swipe(
+                    midX,
+                    height * 6 / 7,
+                    midX,
+                    height / 7
+            );
+        } else {
+            // Swipe from the hotseat to near the top, e.g. 10% of the screen.
+            final UiObject2 hotseat = mHotseat;
+            final Point start = hotseat.getVisibleCenter();
+            final int endY = (int) (mLauncher.getDevice().getDisplayHeight() * 0.1f);
+            mLauncher.swipe(
+                    start.x,
+                    start.y,
+                    start.x,
+                    endY
+            );
+        }
+
+        return new AllApps(mLauncher);
+    }
+
+    /**
+     * Returns an icon for the app, if currently visible.
+     *
+     * @param appName name of the app
+     * @return app icon, if found, null otherwise.
+     */
+    @Nullable
+    public AppIcon tryGetWorkspaceAppIcon(String appName) {
+        final UiObject2 workspace = verifyActiveContainer();
+        final UiObject2 icon = workspace.findObject(AppIcon.getAppIconSelector(appName));
+        return icon != null ? new AppIcon(mLauncher, icon) : null;
+    }
+
+    /**
+     * Ensures that workspace is scrollable. If it's not, drags an icon icons from hotseat to the
+     * second screen.
+     */
+    public void ensureWorkspaceIsScrollable() {
+        final UiObject2 workspace = verifyActiveContainer();
+        if (!isWorkspaceScrollable(workspace)) {
+            dragIconToNextScreen(getHotseatAppIcon("Messages"), workspace);
+        }
+        assertTrue("Home screen workspace didn't become scrollable",
+                isWorkspaceScrollable(workspace));
+    }
+
+    private boolean isWorkspaceScrollable(UiObject2 workspace) {
+        return workspace.isScrollable();
+    }
+
+    @NonNull
+    private AppIcon getHotseatAppIcon(String appName) {
+        return new AppIcon(mLauncher, mLauncher.getObjectInContainer(
+                mHotseat, AppIcon.getAppIconSelector(appName)));
+    }
+
+    private void dragIconToNextScreen(AppIcon app, UiObject2 workspace) {
+        final Point dest = new Point(
+                mLauncher.getDevice().getDisplayWidth(), workspace.getVisibleBounds().centerY());
+        app.getIcon().drag(dest, ICON_DRAG_SPEED);
+        verifyActiveContainer();
+    }
+
+    /**
+     * Flings to get to screens on the right. Waits for scrolling and a possible overscroll
+     * recoil to complete.
+     */
+    public void flingForward() {
+        final UiObject2 workspace = verifyActiveContainer();
+        workspace.fling(Direction.RIGHT);
+        mLauncher.waitForIdle();
+        verifyActiveContainer();
+    }
+
+    /**
+     * Flings to get to screens on the left.  Waits for scrolling and a possible overscroll
+     * recoil to complete.
+     */
+    public void flingBackward() {
+        final UiObject2 workspace = verifyActiveContainer();
+        workspace.fling(Direction.LEFT);
+        mLauncher.waitForIdle();
+        verifyActiveContainer();
+    }
+
+    /**
+     * Opens widgets container by pressing Ctrl+W.
+     *
+     * @return the widgets container.
+     */
+    @NonNull
+    public Widgets openAllWidgets() {
+        verifyActiveContainer();
+        mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
+        return new Widgets(mLauncher);
+    }
+}
\ No newline at end of file