diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java index a205d19f3c..bbc0627517 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java @@ -43,7 +43,7 @@ public class DesktopVisibilityController { */ private boolean isDesktopModeSupported() { return SystemProperties.getBoolean("persist.wm.debug.desktop_mode", false) - || SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false); + || SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false); } /** @@ -81,7 +81,9 @@ public class DesktopVisibilityController { StatefulActivity activity = QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity(); View workspaceView = mLauncher.getWorkspace(); - if (activity == null || workspaceView == null || !isDesktopModeSupported()) return; + if (activity == null || workspaceView == null || !isDesktopModeSupported()) { + return; + } if (mFreeformTasksVisible) { workspaceView.setVisibility(View.INVISIBLE); @@ -93,7 +95,12 @@ public class DesktopVisibilityController { } else { workspaceView.setVisibility(View.VISIBLE); // If freeform isn't visible ensure that launcher appears resumed to behave normally. - activity.setResumed(); + // Check activity state before calling setResumed(). Launcher may have been actually + // paused (eg fullscreen task moved to front). + // In this case we should not mark the activity as resumed. + if (activity.isResumed()) { + activity.setResumed(); + } } } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index a07e4d7122..bb76846c19 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -141,6 +141,7 @@ import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.SplitToWorkspaceController; import com.android.quickstep.util.SplitWithKeyboardShortcutController; import com.android.quickstep.util.TISBindHelper; +import com.android.quickstep.views.DesktopTaskView; import com.android.quickstep.views.OverviewActionsView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; @@ -663,6 +664,20 @@ public class QuickstepLauncher extends Launcher { } } + @Override + public void setResumed() { + if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) { + DesktopVisibilityController controller = mDesktopVisibilityController; + if (controller != null && controller.areFreeformTasksVisible()) { + // Return early to skip setting activity to appear as resumed + // TODO(b/255649902): shouldn't be needed when we have a separate launcher state + // for desktop that we can use to control other parts of launcher + return; + } + } + super.setResumed(); + } + @Override protected void onDeferredResumed() { super.onDeferredResumed(); diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java index 308249cb1e..c878278da5 100644 --- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java @@ -59,10 +59,14 @@ import java.util.function.Consumer; // TODO(b/249371338): TaskView needs to be refactored to have better support for N tasks. public class DesktopTaskView extends TaskView { + /** Flag to indicate whether desktop windowing proto 2 is enabled */ + public static final boolean DESKTOP_IS_PROTO2_ENABLED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_mode_2", false); + /** Flags to indicate whether desktop mode is available on the device */ public static final boolean DESKTOP_MODE_SUPPORTED = SystemProperties.getBoolean("persist.wm.debug.desktop_mode", false) - || SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false); + || DESKTOP_IS_PROTO2_ENABLED; private static final String TAG = DesktopTaskView.class.getSimpleName(); diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml index 81b28ba004..c116c12bad 100644 --- a/res/layout/work_mode_fab.xml +++ b/res/layout/work_mode_fab.xml @@ -12,24 +12,35 @@ See the License for the specific language governing permissions and limitations under the License. --> - \ No newline at end of file + android:contentDescription="@string/work_apps_pause_btn_text" + android:animateLayoutChanges="true"> + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index d66c82496e..b57eb02292 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -149,6 +149,8 @@ 56dp 16dp + 24dp + 8dp 10dp 52dp 16dp diff --git a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java index 74316e22d5..ca08164a05 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java @@ -575,6 +575,10 @@ public abstract class BaseAllAppsContainerView { @@ -666,10 +670,10 @@ public abstract class BaseAllAppsContainerView mAH.get(AdapterHolder.WORK).applyPadding()); - } + + mWorkManager.reset(); + post(() -> mAH.get(AdapterHolder.WORK).applyPadding()); + } else { mWorkManager.detachWorkModeSwitch(); mViewPager = null; diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java index aadd0b52de..c9466a8f43 100644 --- a/src/com/android/launcher3/allapps/WorkModeSwitch.java +++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java @@ -15,17 +15,19 @@ */ package com.android.launcher3.allapps; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TURN_OFF_WORK_APPS_TAP; import static com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.getTabWidth; +import android.animation.LayoutTransition; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; -import android.view.ViewGroup.MarginLayoutParams; import android.view.WindowInsets; -import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.core.graphics.Insets; import androidx.core.view.WindowInsetsCompat; @@ -37,55 +39,62 @@ import com.android.launcher3.anim.KeyboardInsetAnimationCallback; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.StringCache; import com.android.launcher3.views.ActivityContext; -import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip; - /** * Work profile toggle switch shown at the bottom of AllApps work tab */ -public class WorkModeSwitch extends Button implements Insettable, View.OnClickListener, - KeyboardInsetAnimationCallback.KeyboardInsetListener, - PersonalWorkSlidingTabStrip.OnActivePageChangedListener { +public class WorkModeSwitch extends LinearLayout implements Insettable, + KeyboardInsetAnimationCallback.KeyboardInsetListener { private static final int FLAG_FADE_ONGOING = 1 << 1; private static final int FLAG_TRANSLATION_ONGOING = 1 << 2; private static final int FLAG_PROFILE_TOGGLE_ONGOING = 1 << 3; + private static final int SCROLL_THRESHOLD_DP = 10; private final Rect mInsets = new Rect(); private final Rect mImeInsets = new Rect(); private int mFlags; - private boolean mWorkEnabled; - private boolean mOnWorkTab; + private final ActivityContext mActivityContext; - public WorkModeSwitch(Context context) { + // Threshold when user scrolls up/down to determine when should button extend/collapse + private final int mScrollThreshold; + private ImageView mIcon; + private TextView mTextView; + + public WorkModeSwitch(@NonNull Context context) { this(context, null, 0); } - public WorkModeSwitch(Context context, AttributeSet attrs) { + public WorkModeSwitch(@NonNull Context context, @NonNull AttributeSet attrs) { this(context, attrs, 0); } - public WorkModeSwitch(Context context, AttributeSet attrs, int defStyleAttr) { + public WorkModeSwitch(@NonNull Context context, @NonNull AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + mScrollThreshold = Utilities.dpToPx(SCROLL_THRESHOLD_DP); + mActivityContext = ActivityContext.lookupContext(getContext()); } @Override protected void onFinishInflate() { super.onFinishInflate(); + + mIcon = findViewById(R.id.work_icon); + mTextView = findViewById(R.id.pause_text); setSelected(true); - setOnClickListener(this); if (Utilities.ATLEAST_R) { KeyboardInsetAnimationCallback keyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this); setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback); } - ActivityContext activityContext = ActivityContext.lookupContext(getContext()); - DeviceProfile grid = activityContext.getDeviceProfile(); - setInsets(grid.getInsets()); - StringCache cache = activityContext.getStringCache(); + setInsets(mActivityContext.getDeviceProfile().getInsets()); + StringCache cache = mActivityContext.getStringCache(); if (cache != null) { - setText(cache.workProfilePauseButton); + mTextView.setText(cache.workProfilePauseButton); } + + mIcon.setColorFilter(mTextView.getCurrentTextColor()); + getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); } @Override @@ -102,8 +111,6 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi if (!dp.isGestureMode && dp.isTaskbarPresent) { bottomMargin += dp.taskbarSize; - } else { - bottomMargin += insets.bottom; } lp.bottomMargin = bottomMargin; @@ -113,58 +120,32 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile(); View parent = (View) getParent(); + int allAppsLeftRightPadding = mActivityContext.getDeviceProfile().allAppsLeftRightPadding; int size = parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight() - - 2 * dp.allAppsLeftRightPadding; + - 2 * allAppsLeftRightPadding; int tabWidth = getTabWidth(getContext(), size); - int shift = (size - tabWidth) / 2 + dp.allAppsLeftRightPadding; + int shift = (size - tabWidth) / 2 + allAppsLeftRightPadding; setTranslationX(Utilities.isRtl(getResources()) ? shift : -shift); } - @Override - public void onActivePageChanged(int page) { - mOnWorkTab = page == ActivityAllAppsContainerView.AdapterHolder.WORK; - updateVisibility(); - } - - @Override - public void onClick(View view) { - if (Utilities.ATLEAST_P && isEnabled()) { - setFlag(FLAG_PROFILE_TOGGLE_ONGOING); - ActivityContext activityContext = ActivityContext.lookupContext(getContext()); - activityContext.getStatsLogManager().logger().log(LAUNCHER_TURN_OFF_WORK_APPS_TAP); - activityContext.getAppsView().getWorkManager().setWorkProfileEnabled(false); - } - } - @Override public boolean isEnabled() { return super.isEnabled() && getVisibility() == VISIBLE && mFlags == 0; } - /** - * Sets the enabled or disabled state of the button - */ - public void updateCurrentState(boolean isEnabled) { - removeFlag(FLAG_PROFILE_TOGGLE_ONGOING); - if (mWorkEnabled != isEnabled) { - mWorkEnabled = isEnabled; - updateVisibility(); - } - } - - private void updateVisibility() { + public void animateVisibility(boolean visible) { clearAnimation(); - if (mWorkEnabled && mOnWorkTab) { + if (visible) { setFlag(FLAG_FADE_ONGOING); setVisibility(VISIBLE); + extend(); animate().alpha(1).withEndAction(() -> removeFlag(FLAG_FADE_ONGOING)).start(); } else if (getVisibility() != GONE) { setFlag(FLAG_FADE_ONGOING); animate().alpha(0).withEndAction(() -> { removeFlag(FLAG_FADE_ONGOING); - this.setVisibility(GONE); + setVisibility(GONE); }).start(); } } @@ -213,4 +194,16 @@ public class WorkModeSwitch extends Button implements Insettable, View.OnClickLi private void removeFlag(int flag) { mFlags &= ~flag; } + + public void extend() { + mTextView.setVisibility(VISIBLE); + } + + public void shrink(){ + mTextView.setVisibility(GONE); + } + + public int getScrollThreshold() { + return mScrollThreshold; + } } diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java index cfac985c2d..547b74c833 100644 --- a/src/com/android/launcher3/allapps/WorkProfileManager.java +++ b/src/com/android/launcher3/allapps/WorkProfileManager.java @@ -17,6 +17,10 @@ package com.android.launcher3.allapps; import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_DISABLED_CARD; import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_WORK_EDU_CARD; +import static com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder.MAIN; +import static com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder.SEARCH; +import static com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder.WORK; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TURN_OFF_WORK_APPS_TAP; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION; import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED; @@ -28,14 +32,19 @@ import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; +import android.view.View; import androidx.annotation.IntDef; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.R; +import com.android.launcher3.Utilities; import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.views.ActivityContext; import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip; import java.lang.annotation.Retention; @@ -104,8 +113,16 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP @Override public void onActivePageChanged(int page) { + updateWorkFAB(page); + } + + private void updateWorkFAB(int page) { if (mWorkModeSwitch != null) { - mWorkModeSwitch.onActivePageChanged(page); + if (page == MAIN || page == SEARCH) { + mWorkModeSwitch.animateVisibility(false); + } else if (page == WORK && mCurrentState == STATE_ENABLED) { + mWorkModeSwitch.animateVisibility(true); + } } } @@ -123,7 +140,12 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP getAH().mAppsList.updateAdapterItems(); } if (mWorkModeSwitch != null) { - mWorkModeSwitch.updateCurrentState(currentState == STATE_ENABLED); + updateWorkFAB(mAllApps.getCurrentPage()); + } + if (mCurrentState == STATE_ENABLED) { + attachWorkModeSwitch(); + } else if (mCurrentState == STATE_DISABLED) { + detachWorkModeSwitch(); } } @@ -140,13 +162,16 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP mWorkModeSwitch = (WorkModeSwitch) mAllApps.getLayoutInflater().inflate( R.layout.work_mode_fab, mAllApps, false); } - if (mWorkModeSwitch.getParent() != mAllApps) { + if (mWorkModeSwitch.getParent() == null) { mAllApps.addView(mWorkModeSwitch); } + if (mAllApps.getCurrentPage() != WORK) { + mWorkModeSwitch.animateVisibility(false); + } if (getAH() != null) { getAH().applyPadding(); } - mWorkModeSwitch.updateCurrentState(mCurrentState == STATE_ENABLED); + mWorkModeSwitch.setOnClickListener(this::onWorkFabClicked); return true; } /** @@ -169,7 +194,7 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP } private BaseAllAppsContainerView.AdapterHolder getAH() { - return mAllApps.mAH.get(BaseAllAppsContainerView.AdapterHolder.WORK); + return mAllApps.mAH.get(WORK); } public int getCurrentState() { @@ -199,4 +224,40 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP private boolean isEduSeen() { return mPreferences.getInt(KEY_WORK_EDU_STEP, 0) != 0; } + + private void onWorkFabClicked(View view) { + if (Utilities.ATLEAST_P && mCurrentState == STATE_ENABLED && mWorkModeSwitch.isEnabled()) { + ActivityContext activityContext = ActivityContext.lookupContext( + mWorkModeSwitch.getContext()); + activityContext.getStatsLogManager().logger().log(LAUNCHER_TURN_OFF_WORK_APPS_TAP); + setWorkProfileEnabled(false); + } + } + + public RecyclerView.OnScrollListener newScrollListener() { + return new RecyclerView.OnScrollListener() { + int totalDelta = 0; + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState){ + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + totalDelta = 0; + } + } + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + WorkModeSwitch fab = getWorkModeSwitch(); + if (fab == null){ + return; + } + totalDelta = Utilities.boundToRange(totalDelta, + -fab.getScrollThreshold(), fab.getScrollThreshold()) + dy; + boolean isScrollAtTop = recyclerView.computeVerticalScrollOffset() == 0; + if ((isScrollAtTop || totalDelta < -fab.getScrollThreshold())) { + fab.extend(); + } else if (totalDelta > fab.getScrollThreshold()) { + fab.shrink(); + } + } + }; + } } diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index bb3b939550..b46e43f63b 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -92,6 +92,10 @@ public final class FeatureFlags { public static final BooleanFlag ENABLE_HIDE_HEADER = new DeviceFlag("ENABLE_HIDE_HEADER", true, "Hide header on keyboard before typing in all apps"); + public static final BooleanFlag ENABLE_EXPANDING_PAUSE_WORK_BUTTON = new DeviceFlag( + "ENABLE_EXPANDING_PAUSE_WORK_BUTTON", false, + "Expand and collapse pause work button while scrolling"); + public static final BooleanFlag ENABLE_HIDE_HEADER_STATIC = new DeviceFlag( "ENABLE_HIDE_HEADER_STATIC", false, "Hide keyboard suggestion strip"); diff --git a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java index 98eb32e818..10afe13d62 100644 --- a/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java +++ b/tests/tapl/com/android/launcher3/tapl/AddToHomeScreenPrompt.java @@ -45,7 +45,8 @@ public class AddToHomeScreenPrompt { mLauncher.clickObject( mLauncher.waitForObjectInContainer( mWidgetCell.getParent().getParent().getParent().getParent(), - By.text(ADD_AUTOMATICALLY))); + By.text(ADD_AUTOMATICALLY)), + LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER); mLauncher.waitUntilLauncherObjectGone(getSelector()); } } diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index 302fabd92a..ae9ba6787b 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -1466,19 +1466,22 @@ public final class LauncherInstrumentation { return getRealDisplaySize().x - getWindowInsets().right - 1; } - void clickObject(UiObject2 object) { - waitForObjectEnabled(object, "clickObject"); - if (!isLauncher3() && getNavigationModel() != NavigationModel.THREE_BUTTON) { - expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS); - expectEvent(TestProtocol.SEQUENCE_TIS, LauncherInstrumentation.EVENT_TOUCH_UP_TIS); - } - object.click(); + /** + * Click on the ui object right away without waiting for animation. + * + * [UiObject2.click] would wait for all animations finished before clicking. Not waiting for + * animations because in some scenarios there is a playing animations when the click is + * attempted. + */ + void clickObject(UiObject2 uiObject, GestureScope gestureScope) { + final long clickTime = SystemClock.uptimeMillis(); + final Point center = uiObject.getVisibleCenter(); + sendPointer(clickTime, clickTime, MotionEvent.ACTION_DOWN, center, gestureScope); + sendPointer(clickTime, clickTime, MotionEvent.ACTION_UP, center, gestureScope); } void clickLauncherObject(UiObject2 object) { - expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_DOWN); - expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_TOUCH_UP); - clickObject(object); + clickObject(object, GestureScope.INSIDE); } void scrollToLastVisibleRow( diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java index b2a2937396..2c82c50eca 100644 --- a/tests/tapl/com/android/launcher3/tapl/Workspace.java +++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java @@ -385,7 +385,7 @@ public final class Workspace extends Home { Until.hasObject(installerAlert), LauncherInstrumentation.WAIT_TIME_MS)); final UiObject2 ok = device.findObject(By.text("OK")); assertNotNull("OK button is not shown", ok); - launcher.clickObject(ok); + launcher.clickObject(ok, LauncherInstrumentation.GestureScope.OUTSIDE_WITHOUT_PILFER); assertTrue("Uninstall alert is not dismissed after clicking OK", device.wait( Until.gone(installerAlert), LauncherInstrumentation.WAIT_TIME_MS));