Merge "Increasing wait time for Launcher restart after enabling test provider" into main
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index fe517fa..0fda0bf 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -31,7 +31,7 @@
android:layout_height="1dp"
android:layout_weight="1" />
- <com.android.quickstep.views.ScreenshotActionButton
+ <Button
android:id="@+id/action_screenshot"
style="@style/OverviewActionButton"
android:layout_width="wrap_content"
@@ -40,12 +40,17 @@
android:text="@string/action_screenshot"
android:theme="@style/ThemeControlHighlightWorkspaceColor" />
- <com.android.quickstep.views.SplitActionButton
+ <Space
+ android:id="@+id/action_split_space"
+ android:layout_width="@dimen/overview_actions_button_spacing"
+ android:layout_height="1dp"
+ android:visibility="gone" />
+
+ <Button
android:id="@+id/action_split"
style="@style/OverviewActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/overview_actions_button_spacing"
android:text="@string/action_split"
android:theme="@style/ThemeControlHighlightWorkspaceColor"
android:visibility="gone" />
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 9ee9d85..8313e09 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -81,6 +81,7 @@
import android.util.Log;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
import android.view.View;
import android.view.View.OnApplyWindowInsetsListener;
import android.view.ViewGroup;
@@ -179,7 +180,7 @@
protected @Nullable RecentsAnimationController mRecentsAnimationController;
protected @Nullable RecentsAnimationController mDeferredCleanupRecentsAnimationController;
protected RecentsAnimationTargets mRecentsAnimationTargets;
- protected T mActivity;
+ protected @Nullable T mActivity;
protected @Nullable Q mRecentsView;
protected Runnable mGestureEndCallback;
protected MultiStateCallback mStateCallback;
@@ -549,7 +550,7 @@
private void onLauncherStart() {
final T activity = mActivityInterface.getCreatedActivity();
- if (mActivity != activity) {
+ if (activity == null || mActivity != activity) {
return;
}
if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
@@ -922,6 +923,7 @@
// needs to be canceled
mRecentsAnimationController.setWillFinishToHome(swipeUpThresholdPassed);
+ if (mActivity == null) return;
if (swipeUpThresholdPassed) {
mActivity.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, 0);
} else {
@@ -1497,7 +1499,9 @@
if (mGestureState.getEndTarget().isLauncher) {
// This is also called when the launcher is resumed, in order to clear the pending
// widgets that have yet to be configured.
- DragView.removeAllViews(mActivity);
+ if (mActivity != null) {
+ DragView.removeAllViews(mActivity);
+ }
TaskStackChangeListeners.getInstance().registerTaskStackListener(
mActivityRestartListener);
@@ -1860,11 +1864,9 @@
}
public void onConsumerAboutToBeSwitched() {
- if (mActivity != null) {
- // In the off chance that the gesture ends before Launcher is started, we should clear
- // the callback here so that it doesn't update with the wrong state
- resetLauncherListeners();
- }
+ // In the off chance that the gesture ends before Launcher is started, we should clear
+ // the callback here so that it doesn't update with the wrong state
+ resetLauncherListeners();
if (mGestureState.isRecentsAnimationRunning() && mGestureState.getEndTarget() != null
&& !mGestureState.getEndTarget().isLauncher) {
// Continued quick switch.
@@ -1999,11 +2001,12 @@
* continued quick switch gesture, which cancels the previous handler but doesn't invalidate it.
*/
private void resetLauncherListeners() {
- mActivity.removeEventCallback(EVENT_STARTED, mLauncherOnStartCallback);
- mActivity.removeEventCallback(EVENT_DESTROYED, mLauncherOnDestroyCallback);
+ if (mActivity != null) {
+ mActivity.removeEventCallback(EVENT_STARTED, mLauncherOnStartCallback);
+ mActivity.removeEventCallback(EVENT_DESTROYED, mLauncherOnDestroyCallback);
- mActivity.getRootView().setOnApplyWindowInsetsListener(null);
-
+ mActivity.getRootView().setOnApplyWindowInsetsListener(null);
+ }
if (mRecentsView != null) {
mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener);
}
@@ -2039,10 +2042,12 @@
// Update the screenshot of the task
if (shouldUpdate) {
UI_HELPER_EXECUTOR.execute(() -> {
- if (mRecentsAnimationController == null) return;
+ RecentsAnimationController recentsAnimationController =
+ mRecentsAnimationController;
+ if (recentsAnimationController == null) return;
for (int id : runningTaskIds) {
mTaskSnapshotCache.put(
- id, mRecentsAnimationController.screenshotTask(id));
+ id, recentsAnimationController.screenshotTask(id));
}
MAIN_EXECUTOR.execute(() -> {
@@ -2312,7 +2317,7 @@
}
@Override
- public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+ public void onRecentsAnimationFinished(@NonNull RecentsAnimationController controller) {
mRecentsAnimationController = null;
mRecentsAnimationTargets = null;
if (mRecentsView != null) {
@@ -2322,79 +2327,94 @@
@Override
public void onTasksAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTargets) {
- if (mRecentsAnimationController != null) {
- boolean hasStartedTaskBefore = Arrays.stream(appearedTaskTargets).anyMatch(
- mGestureState.mLastStartedTaskIdPredicate);
- if (!mStateCallback.hasStates(STATE_GESTURE_COMPLETED) && !hasStartedTaskBefore) {
- // This is a special case, if a task is started mid-gesture that wasn't a part of a
- // previous quickswitch task launch, then cancel the animation back to the app
- RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0];
- TaskInfo taskInfo = appearedTaskTarget.taskInfo;
- ActiveGestureLog.INSTANCE.addLog(
- new ActiveGestureLog.CompoundString("Unexpected task appeared")
- .append(" id=")
- .append(taskInfo.taskId)
- .append(" pkg=")
- .append(taskInfo.baseIntent.getComponent().getPackageName()));
- finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
- } else if (handleTaskAppeared(appearedTaskTargets)) {
- Optional<RemoteAnimationTarget> taskTargetOptional =
- Arrays.stream(appearedTaskTargets)
- .filter(mGestureState.mLastStartedTaskIdPredicate)
- .findFirst();
- if (!taskTargetOptional.isPresent()) {
- ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id");
- finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
- return;
- }
- RemoteAnimationTarget taskTarget = taskTargetOptional.get();
- TaskView taskView = mRecentsView == null
- ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
- if (taskView == null || !taskView.getThumbnail().shouldShowSplashView()) {
- ActiveGestureLog.INSTANCE.addLog("Invalid task view splash state");
- finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
- return;
- }
-
- ViewGroup splashView = mActivity.getDragLayer();
- final QuickstepLauncher quickstepLauncher = mActivity instanceof QuickstepLauncher
- ? (QuickstepLauncher) mActivity : null;
- if (quickstepLauncher != null) {
- quickstepLauncher.getDepthController().pauseBlursOnWindows(true);
- }
-
- // When revealing the app with launcher splash screen, make the app visible
- // and behind the splash view before the splash is animated away.
- SurfaceTransactionApplier surfaceApplier =
- new SurfaceTransactionApplier(splashView);
- SurfaceTransaction transaction = new SurfaceTransaction();
- for (RemoteAnimationTarget target : appearedTaskTargets) {
- transaction.forSurface(target.leash).setAlpha(1).setLayer(-1).setShow();
- }
- surfaceApplier.scheduleApply(transaction);
-
- SplashScreenExitAnimationUtils.startAnimations(splashView, taskTarget.leash,
- mSplashMainWindowShiftLength, new TransactionPool(), new Rect(),
- SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION,
- /* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0,
- SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION,
- new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- // Hiding launcher which shows the app surface behind, then
- // finishing recents to the app. After transition finish, showing
- // the views on launcher again, so it can be visible when next
- // animation starts.
- splashView.setAlpha(0);
- if (quickstepLauncher != null) {
- quickstepLauncher.getDepthController()
- .pauseBlursOnWindows(false);
- }
- finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1));
- }
- });
- }
+ if (mRecentsAnimationController == null) {
+ return;
}
+ boolean hasStartedTaskBefore = Arrays.stream(appearedTaskTargets).anyMatch(
+ mGestureState.mLastStartedTaskIdPredicate);
+ if (!mStateCallback.hasStates(STATE_GESTURE_COMPLETED) && !hasStartedTaskBefore) {
+ // This is a special case, if a task is started mid-gesture that wasn't a part of a
+ // previous quickswitch task launch, then cancel the animation back to the app
+ RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0];
+ TaskInfo taskInfo = appearedTaskTarget.taskInfo;
+ ActiveGestureLog.INSTANCE.addLog(
+ new ActiveGestureLog.CompoundString("Unexpected task appeared")
+ .append(" id=")
+ .append(taskInfo.taskId)
+ .append(" pkg=")
+ .append(taskInfo.baseIntent.getComponent().getPackageName()));
+ finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+ return;
+ }
+ if (!handleTaskAppeared(appearedTaskTargets)) {
+ return;
+ }
+ Optional<RemoteAnimationTarget> taskTargetOptional =
+ Arrays.stream(appearedTaskTargets)
+ .filter(mGestureState.mLastStartedTaskIdPredicate)
+ .findFirst();
+ if (!taskTargetOptional.isPresent()) {
+ ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id");
+ finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+ return;
+ }
+ RemoteAnimationTarget taskTarget = taskTargetOptional.get();
+ TaskView taskView = mRecentsView == null
+ ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
+ if (taskView == null || !taskView.getThumbnail().shouldShowSplashView()) {
+ ActiveGestureLog.INSTANCE.addLog("Invalid task view splash state");
+ finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+ return;
+ }
+ if (mActivity == null) {
+ ActiveGestureLog.INSTANCE.addLog("Activity destroyed");
+ finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
+ return;
+ }
+ animateSplashScreenExit(mActivity, appearedTaskTargets, taskTarget.leash);
+ }
+
+ private void animateSplashScreenExit(
+ @NonNull T activity,
+ @NonNull RemoteAnimationTarget[] appearedTaskTargets,
+ @NonNull SurfaceControl leash) {
+ ViewGroup splashView = activity.getDragLayer();
+ final QuickstepLauncher quickstepLauncher = activity instanceof QuickstepLauncher
+ ? (QuickstepLauncher) activity : null;
+ if (quickstepLauncher != null) {
+ quickstepLauncher.getDepthController().pauseBlursOnWindows(true);
+ }
+
+ // When revealing the app with launcher splash screen, make the app visible
+ // and behind the splash view before the splash is animated away.
+ SurfaceTransactionApplier surfaceApplier =
+ new SurfaceTransactionApplier(splashView);
+ SurfaceTransaction transaction = new SurfaceTransaction();
+ for (RemoteAnimationTarget target : appearedTaskTargets) {
+ transaction.forSurface(target.leash).setAlpha(1).setLayer(-1).setShow();
+ }
+ surfaceApplier.scheduleApply(transaction);
+
+ SplashScreenExitAnimationUtils.startAnimations(splashView, leash,
+ mSplashMainWindowShiftLength, new TransactionPool(), new Rect(),
+ SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION,
+ /* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0,
+ SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION,
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Hiding launcher which shows the app surface behind, then
+ // finishing recents to the app. After transition finish, showing
+ // the views on launcher again, so it can be visible when next
+ // animation starts.
+ splashView.setAlpha(0);
+ if (quickstepLauncher != null) {
+ quickstepLauncher.getDepthController()
+ .pauseBlursOnWindows(false);
+ }
+ finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1));
+ }
+ });
}
private void finishRecentsAnimationOnTasksAppeared(Runnable onFinishComplete) {
diff --git a/quickstep/src/com/android/quickstep/views/ActionButton.kt b/quickstep/src/com/android/quickstep/views/ActionButton.kt
deleted file mode 100644
index 5c004cc..0000000
--- a/quickstep/src/com/android/quickstep/views/ActionButton.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2023 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.quickstep.views
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.Button
-
-/**
- * A button on the Overview Actions Bar. Custom logic for hiding/showing each button type is handled
- * in the respective subclass.
- */
-open class ActionButton : Button {
- private var mHiddenFlags = 0
-
- constructor(context: Context) : super(context)
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
- constructor(
- context: Context,
- attrs: AttributeSet?,
- defStyleAttr: Int
- ) : super(context, attrs, defStyleAttr)
-
- /**
- * Updates the proper flags to indicate whether the button should be hidden.
- *
- * @param flag The flag to update.
- * @param enable Whether to enable the hidden flag: True will cause view to be hidden.
- */
- protected fun updateHiddenFlags(flag: Int, enable: Boolean) {
- if (enable) {
- mHiddenFlags = mHiddenFlags or flag
- } else {
- mHiddenFlags = mHiddenFlags and flag.inv()
- }
- val shouldBeVisible = mHiddenFlags == 0
- this.visibility = if (shouldBeVisible) VISIBLE else GONE
- }
-
- /** Show/hide the button when the focused task is a single/pair. */
- open fun updateForMultipleTasks(hasMultipleTasks: Boolean) {
- // overridden in subclass, or else don't do anything
- }
-
- /** Show/hide the button depending on if the device is a tablet. */
- open fun updateForTablet(isTablet: Boolean) {
- // overridden in subclass, or else don't do anything
- }
-}
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index 8692e4e..7f1d619 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -22,6 +22,7 @@
import android.util.AttributeSet;
import android.view.View;
import android.view.View.OnClickListener;
+import android.widget.Button;
import android.widget.FrameLayout;
import androidx.annotation.IntDef;
@@ -40,8 +41,6 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.List;
/**
* View for showing action buttons in Overview
@@ -90,11 +89,14 @@
private static final int INDEX_SCROLL_ALPHA = 5;
private static final int NUM_ALPHAS = 6;
- private MultiValueAlpha mMultiValueAlpha;
+ public @interface SplitButtonHiddenFlags { }
+ public static final int FLAG_IS_NOT_TABLET = 1 << 0;
- private List<ActionButton> mActionButtons = new ArrayList<>();
- private ScreenshotActionButton mScreenshotButton;
- private SplitActionButton mSplitButton;
+ public @interface SplitButtonDisabledFlags { }
+ public static final int FLAG_SINGLE_TASK = 1 << 0;
+
+ private MultiValueAlpha mMultiValueAlpha;
+ private Button mSplitButton;
@ActionsHiddenFlags
private int mHiddenFlags;
@@ -102,6 +104,12 @@
@ActionsDisabledFlags
protected int mDisabledFlags;
+ @SplitButtonHiddenFlags
+ private int mSplitButtonHiddenFlags;
+
+ @SplitButtonDisabledFlags
+ private int mSplitButtonDisabledFlags;
+
@Nullable
protected T mCallbacks;
@@ -127,12 +135,9 @@
mMultiValueAlpha = new MultiValueAlpha(findViewById(R.id.action_buttons), NUM_ALPHAS);
mMultiValueAlpha.setUpdateVisibility(true);
- mScreenshotButton = findViewById(R.id.action_screenshot);
- mScreenshotButton.setOnClickListener(this);
- mActionButtons.add(mScreenshotButton);
+ findViewById(R.id.action_screenshot).setOnClickListener(this);
mSplitButton = findViewById(R.id.action_split);
mSplitButton.setOnClickListener(this);
- mActionButtons.add(mSplitButton);
}
/**
@@ -196,28 +201,40 @@
}
boolean isEnabled = (mDisabledFlags & ~DISABLED_ROTATED) == 0;
LayoutUtils.setViewEnabled(this, isEnabled);
+ updateSplitButtonEnabledState();
}
/**
- * Updates flags to hide and show actions buttons when a grouped task (split screen) is focused.
- * @param isGroupedTask True if the focused task is a grouped task.
+ * Updates the proper flags to indicate whether the "Split screen" button should be hidden.
+ *
+ * @param flag The flag to update.
+ * @param enable Whether to enable the hidden flag: True will cause view to be hidden.
*/
- public void updateForGroupedTask(boolean isGroupedTask) {
- for (ActionButton button : mActionButtons) {
- // Update flags to show/hide buttons.
- button.updateForMultipleTasks(isGroupedTask);
+ public void updateSplitButtonHiddenFlags(@SplitButtonHiddenFlags int flag, boolean enable) {
+ if (enable) {
+ mSplitButtonHiddenFlags |= flag;
+ } else {
+ mSplitButtonHiddenFlags &= ~flag;
}
+ if (mSplitButton == null) return;
+ boolean shouldBeVisible = mSplitButtonHiddenFlags == 0;
+ mSplitButton.setVisibility(shouldBeVisible ? VISIBLE : GONE);
+ findViewById(R.id.action_split_space).setVisibility(shouldBeVisible ? VISIBLE : GONE);
}
/**
- * Updates flags to hide and show actions buttons depending on if the device is a tablet.
- * @param isTablet True if the current device is a tablet.
+ * Updates the proper flags to indicate whether the "Split screen" button should be disabled.
+ *
+ * @param flag The flag to update.
+ * @param enable Whether to enable the disable flag: True will cause view to be disabled.
*/
- public void updateForTablet(boolean isTablet) {
- for (ActionButton button : mActionButtons) {
- // Update flags to show/hide buttons.
- button.updateForTablet(isTablet);
+ public void updateSplitButtonDisabledFlags(@SplitButtonDisabledFlags int flag, boolean enable) {
+ if (enable) {
+ mSplitButtonDisabledFlags |= flag;
+ } else {
+ mSplitButtonDisabledFlags &= ~flag;
}
+ updateSplitButtonEnabledState();
}
public MultiProperty getContentAlpha() {
@@ -295,4 +312,18 @@
(dp.isLandscape ? R.drawable.ic_split_horizontal : R.drawable.ic_split_vertical),
0, 0, 0);
}
+
+ /**
+ * Enables/disables the "Split" button based on the status of mSplitButtonDisabledFlags and
+ * mDisabledFlags.
+ */
+ private void updateSplitButtonEnabledState() {
+ if (mSplitButton == null) {
+ return;
+ }
+ boolean isParentEnabled = (mDisabledFlags & ~DISABLED_ROTATED) == 0;
+ boolean shouldBeEnabled = mSplitButtonDisabledFlags == 0 && isParentEnabled;
+ mSplitButton.setEnabled(shouldBeEnabled);
+ }
+
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 6a275e6..7972999 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -20,6 +20,7 @@
import static android.view.Surface.ROTATION_0;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
+
import static com.android.app.animation.Interpolators.ACCELERATE;
import static com.android.app.animation.Interpolators.ACCELERATE_0_75;
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
@@ -33,7 +34,6 @@
import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
-import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
import static com.android.launcher3.LauncherState.BACKGROUND_APP;
@@ -42,6 +42,7 @@
import static com.android.launcher3.Utilities.mapToRange;
import static com.android.launcher3.Utilities.squaredHypot;
import static com.android.launcher3.Utilities.squaredTouchSlop;
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_ACTIONS_SPLIT;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
@@ -56,6 +57,8 @@
import static com.android.quickstep.util.LogUtils.splitFailureMessage;
import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
+import static com.android.quickstep.views.OverviewActionsView.FLAG_IS_NOT_TABLET;
+import static com.android.quickstep.views.OverviewActionsView.FLAG_SINGLE_TASK;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
@@ -3924,23 +3927,18 @@
}
/**
- * Hides all overview actions if user is halfway through split selection, shows otherwise.
- * We only show split option if:
- * * Focused view is a single app
+ * Hides all overview actions if current page is for split apps, shows otherwise
+ * If actions are showing, we only show split option if
* * Device is large screen
+ * * There are at least 2 tasks to invoke split
*/
private void updateCurrentTaskActionsVisibility() {
- boolean isGroupedTask = getCurrentPageTaskView() != null
- && getCurrentPageTaskView().containsMultipleTasks();
- // Update flags to see if entire actions bar should be hidden.
- mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SCREEN, isGroupedTask);
+ boolean isCurrentSplit = getCurrentPageTaskView() instanceof GroupedTaskView;
+ mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SCREEN, isCurrentSplit);
mActionsView.updateHiddenFlags(HIDDEN_SPLIT_SELECT_ACTIVE, isSplitSelectionActive());
- // Update flags to see if actions bar should show buttons for a single task or a pair of
- // tasks.
- mActionsView.updateForGroupedTask(isGroupedTask);
- // Update flags to see if split button should be hidden.
- mActionsView.updateForTablet(mActivity.getDeviceProfile().isTablet);
-
+ mActionsView.updateSplitButtonHiddenFlags(FLAG_IS_NOT_TABLET,
+ !mActivity.getDeviceProfile().isTablet);
+ mActionsView.updateSplitButtonDisabledFlags(FLAG_SINGLE_TASK, /*enable=*/ false);
if (isDesktopModeSupported()) {
boolean isCurrentDesktop = getCurrentPageTaskView() instanceof DesktopTaskView;
mActionsView.updateHiddenFlags(HIDDEN_DESKTOP, isCurrentDesktop);
diff --git a/quickstep/src/com/android/quickstep/views/ScreenshotActionButton.kt b/quickstep/src/com/android/quickstep/views/ScreenshotActionButton.kt
deleted file mode 100644
index 9cb48b8..0000000
--- a/quickstep/src/com/android/quickstep/views/ScreenshotActionButton.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2023 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.quickstep.views
-
-import android.content.Context
-import android.util.AttributeSet
-
-/** A button on the Overview Actions Bar for screenshotting the focused app. */
-class ScreenshotActionButton : ActionButton {
- companion object {
- const val FLAG_MULTIPLE_TASKS_HIDE_SCREENSHOT = 1 shl 0
- }
-
- constructor(context: Context) : super(context)
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
- constructor(
- context: Context,
- attrs: AttributeSet?,
- defStyleAttr: Int
- ) : super(context, attrs, defStyleAttr)
-
- /** Show/hide the button when the focused task is a single/pair. */
- override fun updateForMultipleTasks(hasMultipleTasks: Boolean) {
- // Hidden for multiple tasks
- updateHiddenFlags(FLAG_MULTIPLE_TASKS_HIDE_SCREENSHOT, hasMultipleTasks)
- }
-}
diff --git a/quickstep/src/com/android/quickstep/views/SplitActionButton.kt b/quickstep/src/com/android/quickstep/views/SplitActionButton.kt
deleted file mode 100644
index 88d1a77..0000000
--- a/quickstep/src/com/android/quickstep/views/SplitActionButton.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2023 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.quickstep.views
-
-import android.content.Context
-import android.util.AttributeSet
-
-/** A button on the Overview Actions Bar for initiating split screen. */
-class SplitActionButton : ActionButton {
- companion object {
- const val FLAG_IS_NOT_TABLET_HIDE_SPLIT = 1 shl 0
- const val FLAG_MULTIPLE_TASKS_HIDE_SPLIT = 1 shl 1
- }
-
- constructor(context: Context) : super(context)
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
- constructor(
- context: Context,
- attrs: AttributeSet?,
- defStyleAttr: Int
- ) : super(context, attrs, defStyleAttr)
-
- /** Show/hide the button when the focused task is a single/pair. */
- override fun updateForMultipleTasks(hasMultipleTasks: Boolean) {
- // Hidden for multiple tasks
- updateHiddenFlags(FLAG_MULTIPLE_TASKS_HIDE_SPLIT, hasMultipleTasks)
- }
-
- /** Show/hide the button depending on if the device is a tablet. */
- override fun updateForTablet(isTablet: Boolean) {
- // Hidden for non-tablets
- updateHiddenFlags(FLAG_IS_NOT_TABLET_HIDE_SPLIT, !isTablet)
- }
-}
diff --git a/res/drawable/bg_ps_header.xml b/res/drawable/bg_ps_header.xml
new file mode 100644
index 0000000..526bb5a
--- /dev/null
+++ b/res/drawable/bg_ps_header.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/ps_container_corner_radius" />
+ <solid android:color="?attr/materialColorSurfaceContainerHigh" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/bg_ps_lock_button.xml b/res/drawable/bg_ps_lock_button.xml
new file mode 100644
index 0000000..aef1e81
--- /dev/null
+++ b/res/drawable/bg_ps_lock_button.xml
@@ -0,0 +1,32 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/ps_lock_button_width"
+ android:height="@dimen/ps_button_height"
+ android:viewportWidth="89"
+ android:viewportHeight="36">
+ <path
+ android:pathData="M18,0L71,0A18,18 0,0 1,89 18L89,18A18,18 0,0 1,71 36L18,36A18,18 0,0 1,0 18L0,18A18,18 0,0 1,18 0z"
+ android:fillColor="?attr/materialColorPrimaryFixedDim"/>
+ <path
+ android:pathData="M26.167,14.667H27C27.917,14.667 28.667,15.417 28.667,16.333V24.667C28.667,25.583 27.917,26.333 27,26.333H17C16.083,26.333 15.333,25.583 15.333,24.667V16.333C15.333,15.417 16.083,14.667 17,14.667H17.833V13C17.833,10.7 19.7,8.833 22,8.833C24.3,8.833 26.167,10.7 26.167,13V14.667ZM22,10.5C20.617,10.5 19.5,11.617 19.5,13V14.667H24.5V13C24.5,11.617 23.383,10.5 22,10.5ZM17,24.667V16.333H27V24.667H17ZM23.667,20.5C23.667,21.417 22.917,22.167 22,22.167C21.083,22.167 20.333,21.417 20.333,20.5C20.333,19.583 21.083,18.833 22,18.833C22.917,18.833 23.667,19.583 23.667,20.5Z"
+ android:fillColor="?attr/materialColorOnPrimaryFixed"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M41.204,23V12.976H42.73V21.544H47.504V23H41.204ZM52.352,23.224C51.615,23.224 50.976,23.061 50.434,22.734C49.893,22.398 49.473,21.936 49.174,21.348C48.885,20.76 48.74,20.083 48.74,19.318C48.74,18.543 48.885,17.867 49.174,17.288C49.473,16.7 49.893,16.243 50.434,15.916C50.976,15.58 51.615,15.412 52.352,15.412C53.099,15.412 53.738,15.58 54.27,15.916C54.812,16.243 55.227,16.7 55.516,17.288C55.815,17.867 55.964,18.543 55.964,19.318C55.964,20.083 55.815,20.76 55.516,21.348C55.227,21.936 54.812,22.398 54.27,22.734C53.738,23.061 53.099,23.224 52.352,23.224ZM52.352,21.838C52.772,21.838 53.141,21.74 53.458,21.544C53.776,21.348 54.023,21.063 54.2,20.69C54.378,20.307 54.466,19.85 54.466,19.318C54.466,18.777 54.378,18.319 54.2,17.946C54.023,17.573 53.776,17.288 53.458,17.092C53.141,16.896 52.777,16.798 52.366,16.798C51.946,16.798 51.578,16.896 51.26,17.092C50.943,17.288 50.691,17.573 50.504,17.946C50.327,18.319 50.238,18.777 50.238,19.318C50.238,19.859 50.327,20.317 50.504,20.69C50.691,21.063 50.943,21.348 51.26,21.544C51.587,21.74 51.951,21.838 52.352,21.838ZM60.899,23.224C60.199,23.224 59.583,23.065 59.051,22.748C58.528,22.421 58.118,21.964 57.819,21.376C57.529,20.788 57.385,20.102 57.385,19.318C57.385,18.525 57.534,17.839 57.833,17.26C58.141,16.672 58.561,16.219 59.093,15.902C59.634,15.575 60.255,15.412 60.955,15.412C61.832,15.412 62.556,15.631 63.125,16.07C63.694,16.509 64.039,17.111 64.161,17.876L62.705,18.114C62.611,17.713 62.411,17.395 62.103,17.162C61.804,16.919 61.412,16.798 60.927,16.798C60.544,16.798 60.199,16.896 59.891,17.092C59.583,17.279 59.335,17.559 59.149,17.932C58.972,18.305 58.883,18.767 58.883,19.318C58.883,19.859 58.972,20.321 59.149,20.704C59.326,21.077 59.569,21.362 59.877,21.558C60.185,21.745 60.535,21.838 60.927,21.838C61.394,21.838 61.771,21.721 62.061,21.488C62.36,21.255 62.579,20.909 62.719,20.452L64.133,20.788C63.956,21.507 63.596,22.095 63.055,22.552C62.514,23 61.795,23.224 60.899,23.224ZM65.985,23V12.136H67.483V18.688L70.381,15.636H72.187V15.72L69.499,18.492L72.257,22.916V23H70.549L68.435,19.598L67.483,20.564V23H65.985Z"
+ android:fillColor="?attr/materialColorOnPrimaryFixed"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/bg_ps_settings_button.xml b/res/drawable/bg_ps_settings_button.xml
new file mode 100644
index 0000000..c06e0c0
--- /dev/null
+++ b/res/drawable/bg_ps_settings_button.xml
@@ -0,0 +1,35 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/ps_button_height"
+ android:height="@dimen/ps_button_height"
+ android:viewportWidth="40"
+ android:viewportHeight="40">
+ <path
+ android:pathData="M20,0L20,0A20,20 0,0 1,40 20L40,20A20,20 0,0 1,20 40L20,40A20,20 0,0 1,0 20L0,20A20,20 0,0 1,20 0z"
+ android:fillColor="?attr/materialColorSurfaceBright"/>
+ <group>
+ <clip-path
+ android:pathData="M10,10h20v20h-20z"/>
+ <path
+ android:pathData="M21.542,28.542H18.458C17.841,28.542 17.325,28.092 17.25,27.483L17.025,25.908C16.8,25.792 16.583,25.667 16.367,25.525L14.866,26.125C14.283,26.342 13.642,26.1 13.358,25.583L11.833,22.942C11.542,22.392 11.667,21.742 12.133,21.375L13.408,20.383C13.4,20.258 13.392,20.133 13.392,20C13.392,19.875 13.4,19.742 13.408,19.617L12.142,18.625C11.65,18.25 11.525,17.575 11.833,17.058L13.375,14.4C13.658,13.883 14.3,13.65 14.866,13.875L16.375,14.483C16.591,14.342 16.808,14.217 17.025,14.1L17.25,12.508C17.325,11.925 17.841,11.467 18.45,11.467H21.533C22.15,11.467 22.667,11.917 22.742,12.525L22.966,14.1C23.191,14.217 23.408,14.342 23.625,14.483L25.125,13.883C25.716,13.667 26.358,13.908 26.642,14.425L28.175,17.075C28.475,17.625 28.341,18.275 27.875,18.642L26.608,19.633C26.617,19.758 26.625,19.883 26.625,20.017C26.625,20.15 26.617,20.275 26.608,20.4L27.875,21.392C28.341,21.767 28.475,22.417 28.183,22.942L26.633,25.625C26.35,26.142 25.708,26.375 25.133,26.15L23.633,25.55C23.417,25.692 23.2,25.817 22.983,25.933L22.758,27.525C22.675,28.092 22.158,28.542 21.542,28.542ZM21.1,27.267C21.1,27.275 21.1,27.275 21.1,27.283V27.267ZM18.9,27.25V27.267C18.908,27.267 18.908,27.258 18.9,27.25ZM18.85,26.875H21.15L21.458,24.75L21.9,24.567C22.267,24.417 22.633,24.2 23.017,23.917L23.392,23.633L25.375,24.433L26.525,22.433L24.833,21.117L24.892,20.65C24.917,20.433 24.941,20.225 24.941,20C24.941,19.775 24.917,19.558 24.892,19.35L24.833,18.883L26.525,17.567L25.367,15.567L23.375,16.367L23,16.075C22.65,15.808 22.275,15.592 21.892,15.433L21.458,15.25L21.15,13.125H18.85L18.542,15.25L18.1,15.425C17.733,15.583 17.367,15.792 16.983,16.083L16.608,16.358L14.625,15.567L13.467,17.558L15.158,18.875L15.1,19.342C15.075,19.558 15.05,19.783 15.05,20C15.05,20.217 15.066,20.442 15.1,20.65L15.158,21.117L13.467,22.433L14.616,24.433L16.608,23.633L16.983,23.925C17.341,24.2 17.7,24.408 18.091,24.567L18.533,24.75L18.85,26.875ZM25.183,24.767C25.183,24.775 25.175,24.783 25.175,24.792L25.183,24.767ZM14.808,24.758L14.816,24.775C14.816,24.767 14.808,24.758 14.808,24.758ZM25.183,15.225C25.183,15.233 25.191,15.242 25.191,15.242L25.183,15.225ZM14.825,15.208L14.816,15.225C14.816,15.225 14.825,15.217 14.825,15.208ZM21.091,12.733C21.091,12.742 21.091,12.742 21.091,12.75V12.733ZM18.908,12.717V12.733C18.908,12.725 18.908,12.725 18.908,12.717Z"
+ android:fillColor="?attr/materialColorOnSurfaceVariant"/>
+ <path
+ android:pathData="M20,22.917C21.611,22.917 22.916,21.611 22.916,20C22.916,18.389 21.611,17.083 20,17.083C18.389,17.083 17.083,18.389 17.083,20C17.083,21.611 18.389,22.917 20,22.917Z"
+ android:fillColor="?attr/materialColorOnSurfaceVariant"/>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/bg_ps_transition_image.xml b/res/drawable/bg_ps_transition_image.xml
new file mode 100644
index 0000000..dfad3cf
--- /dev/null
+++ b/res/drawable/bg_ps_transition_image.xml
@@ -0,0 +1,35 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="vector"
+ android:width="@dimen/ps_button_height"
+ android:height="@dimen/ps_button_height"
+ android:viewportWidth="40"
+ android:viewportHeight="40">
+ <path
+ android:name="path"
+ android:pathData="M 19.998 36.668 C 10.816 36.668 3.332 29.184 3.332 20 C 3.332 10.818 10.816 3.334 19.998 3.334 C 20.916 3.334 21.666 4.084 21.666 5 C 21.666 5.918 20.916 6.668 19.998 6.668 C 12.648 6.668 6.666 12.65 6.666 20 C 6.666 27.35 12.648 33.334 19.998 33.334 C 27.348 33.334 33.332 27.35 33.332 20 C 33.332 19.084 34.082 18.334 34.998 18.334 C 35.916 18.334 36.666 19.084 36.666 20 C 36.666 29.184 29.182 36.668 19.998 36.668 Z"
+ android:fillColor="?attr/materialColorOnPrimaryFixed"/>
+ <path
+ android:name="path_3"
+ android:pathData="M 20 0 C 25.302 0 30.393 2.109 34.142 5.858 C 37.891 9.607 40 14.698 40 20 C 40 25.302 37.891 30.393 34.142 34.142 C 30.393 37.891 25.302 40 20 40 C 14.698 40 9.607 37.891 5.858 34.142 C 2.109 30.393 0 25.302 0 20 C 0 14.698 2.109 9.607 5.858 5.858 C 9.607 2.109 14.698 0 20 0"
+ android:fillColor="?attr/materialColorPrimaryFixedDim"/>
+ <path
+ android:name="path_4"
+ android:pathData="M 19.999 28.334 C 15.408 28.334 11.666 24.592 11.666 20 C 11.666 15.409 15.408 11.667 19.999 11.667 C 20.458 11.667 20.833 12.042 20.833 12.5 C 20.833 12.959 20.458 13.334 19.999 13.334 C 16.324 13.334 13.333 16.325 13.333 20 C 13.333 23.675 16.324 26.667 19.999 26.667 C 23.674 26.667 26.666 23.675 26.666 20 C 26.666 19.542 27.041 19.167 27.499 19.167 C 27.958 19.167 28.333 19.542 28.333 20 C 28.333 24.592 24.591 28.334 19.999 28.334 Z"
+ android:fillColor="?attr/materialColorOnPrimaryFixed"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/bg_ps_unlock_button.xml b/res/drawable/bg_ps_unlock_button.xml
new file mode 100644
index 0000000..d5eedd2
--- /dev/null
+++ b/res/drawable/bg_ps_unlock_button.xml
@@ -0,0 +1,29 @@
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/ps_button_height"
+ android:height="@dimen/ps_button_height"
+ android:viewportWidth="36"
+ android:viewportHeight="36">
+ <path
+ android:pathData="M18,0L18,0A18,18 0,0 1,36 18L36,18A18,18 0,0 1,18 36L18,36A18,18 0,0 1,0 18L0,18A18,18 0,0 1,18 0z"
+ android:fillColor="?attr/materialColorPrimaryFixedDim"/>
+ <path
+ android:pathData="M22.167,14.667H23C23.917,14.667 24.667,15.417 24.667,16.333V24.667C24.667,25.583 23.917,26.333 23,26.333H13C12.083,26.333 11.333,25.583 11.333,24.667V16.333C11.333,15.417 12.083,14.667 13,14.667H13.833V13C13.833,10.7 15.7,8.833 18,8.833C20.3,8.833 22.167,10.7 22.167,13V14.667ZM18,10.5C16.617,10.5 15.5,11.617 15.5,13V14.667H20.5V13C20.5,11.617 19.383,10.5 18,10.5ZM13,24.667V16.333H23V24.667H13ZM19.667,20.5C19.667,21.417 18.917,22.167 18,22.167C17.083,22.167 16.333,21.417 16.333,20.5C16.333,19.583 17.083,18.833 18,18.833C18.917,18.833 19.667,19.583 19.667,20.5Z"
+ android:fillColor="?attr/materialColorOnPrimaryFixed"
+ android:fillType="evenOdd"/>
+</vector>
\ No newline at end of file
diff --git a/res/layout/private_space_header.xml b/res/layout/private_space_header.xml
new file mode 100644
index 0000000..24e290d
--- /dev/null
+++ b/res/layout/private_space_header.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<RelativeLayout
+ android:id="@+id/ps_header_layout"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/ps_header_height"
+ android:background="@drawable/bg_ps_header"
+ android:clipToOutline="true"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/ps_lock_unlock_button"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/ps_header_image_height"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:background="@android:color/transparent"
+ android:layout_marginEnd="@dimen/ps_header_layout_margin"
+ android:contentDescription="@string/ps_container_lock_unlock_button" />
+
+ <ImageButton
+ android:id="@+id/ps_settings_button"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/ps_header_image_height"
+ android:layout_toStartOf="@+id/ps_lock_unlock_button"
+ android:layout_centerVertical="true"
+ android:background="@android:color/transparent"
+ android:layout_marginEnd="@dimen/ps_header_settings_icon_margin_end"
+ android:src="@drawable/bg_ps_settings_button"
+ android:contentDescription="@string/ps_container_settings" />
+
+ <ImageView
+ android:id="@+id/ps_transition_image"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/ps_header_image_height"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:background="@android:color/transparent"
+ android:layout_marginEnd="@dimen/ps_header_layout_margin"
+ android:src="@drawable/bg_ps_transition_image"
+ android:contentDescription="@string/ps_container_transition" />
+
+ <TextView
+ android:id="@+id/ps_container_header"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/ps_header_text_height"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:gravity="center_vertical"
+ android:layout_marginStart="@dimen/ps_header_layout_margin"
+ android:text="@string/ps_container_title"
+ android:theme="@style/PrivateSpaceHeaderTextStyle"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c0a1e0a..ac701d6 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -456,4 +456,17 @@
<!-- Default Ime height. Used only for logging purposes.
Assume this is default keyboard height in EN locale in case the keyboard height is not known when queried.-->
<dimen name="default_ime_height">300dp</dimen>
+
+ <!-- Private Space parameters -->
+ <dimen name="ps_container_corner_radius">24dp</dimen>
+ <dimen name="ps_header_height">64dp</dimen>
+ <dimen name="ps_header_relative_layout_height">48dp</dimen>
+ <dimen name="ps_header_image_height">36dp</dimen>
+ <dimen name="ps_header_text_height">24dp</dimen>
+ <dimen name="ps_header_layout_margin">16dp</dimen>
+ <dimen name="ps_header_settings_icon_margin_end">8dp</dimen>
+ <dimen name="ps_header_text_size">16sp</dimen>
+ <dimen name="ps_button_height">36dp</dimen>
+ <dimen name="ps_button_width">36dp</dimen>
+ <dimen name="ps_lock_button_width">89dp</dimen>
</resources>
diff --git a/res/values/id.xml b/res/values/id.xml
index 872ae2f..6156c91 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -45,4 +45,10 @@
<item type="id" name="dismiss_view" />
+ <!-- Private Space parameters -->
+ <item type="id" name="ps_container_header" />
+ <item type="id" name="ps_lock_unlock_button" />
+ <item type="id" name="ps_settings_button" />
+ <item type="id" name="ps_transition_image" />
+
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f08f8f0..31579cd 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -454,8 +454,17 @@
<!-- Failed action error message: e.g. Failed: Pause -->
<string name="remote_action_failed">Failed: <xliff:g id="what" example="Pause">%1$s</xliff:g></string>
+ <!-- Strings for Private Space -->
<!-- Private space label -->
<string name="private_space_label">Private space</string>
+ <!-- Title for Private Space Container shown at the bottom of all apps drawer -->
+ <string name="ps_container_title">Private</string>
+ <!-- Description for Private Space Settings button -->
+ <string name="ps_container_settings">Private Space Settings</string>
+ <!-- Description for Private Space Lock/Unlock button -->
+ <string name="ps_container_lock_unlock_button">Lock/Unlock Private Space</string>
+ <!-- Description for Private Space Transition button -->
+ <string name="ps_container_transition">Private Space Transitioning</string>
<!-- Strings for bubble bar -->
<!-- content description for the overflow bubble [CHAR_LIMIT=none] -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 82a227a..36991b1 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -435,4 +435,10 @@
<item name="arrowTipBackground">@color/arrow_tip_view_bg</item>
<item name="arrowTipTextColor">@color/arrow_tip_view_content</item>
</style>
+
+ <style name="PrivateSpaceHeaderTextStyle">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">@color/material_color_on_surface</item>
+ <item name="android:fontFamily">google-sans-text-medium</item>
+ </style>
</resources>
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index fed031b..28c7a37 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -592,7 +592,6 @@
int availableResponsiveWidth =
availableWidthPx - (isVerticalBarLayout() ? hotseatBarSizePx : 0);
int numWorkspaceColumns = getPanelCount() * inv.numColumns;
- int numAllAppsColumns = getPanelCount() * inv.numAllAppsColumns;
// don't use availableHeightPx because it subtracts mInsets.bottom
int availableResponsiveHeight = heightPx - mInsets.top
- (isVerticalBarLayout() ? 0 : hotseatBarSizePx);
@@ -612,7 +611,7 @@
isTwoPanels ? inv.allAppsSpecsTwoPanelId : inv.allAppsSpecsId),
ResponsiveSpecType.AllApps);
mResponsiveAllAppsWidthSpec = allAppsSpecs.getCalculatedSpec(responsiveAspectRatio,
- DimensionType.WIDTH, numAllAppsColumns, availableWidthPx,
+ DimensionType.WIDTH, numShownAllAppsColumns, availableWidthPx,
mResponsiveWorkspaceWidthSpec);
mResponsiveAllAppsHeightSpec = allAppsSpecs.getCalculatedSpec(responsiveAspectRatio,
DimensionType.HEIGHT, inv.numRows, heightPx - mInsets.top,
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index 8ff030e..8eab3e3 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -90,14 +90,23 @@
/**
* Synchronously shows the soft input method.
*
- * @param shouldFocus whether this EditText should also request focus.
- * @return true if the keyboard is shown correctly and focus is given to this view (if
- * applicable).
+ * @return true if the keyboard is shown correctly and focus is given to this view.
*/
- public boolean showKeyboard(boolean shouldFocus) {
+ public boolean showKeyboard() {
onKeyboardShown();
- boolean focusResult = !shouldFocus || requestFocus();
- return focusResult && showSoftInputInternal();
+ return requestFocus() && showSoftInputInternal();
+ }
+
+ /**
+ * Requests the framework to show the keyboard in order to ensure that an already registered
+ * controlled keyboard animation is triggered correctly.
+ * Must NEVER be called in any other case than to trigger a pre-registered controlled animation.
+ */
+ public void requestShowKeyboardForControlledAnimation() {
+ // We don't log the keyboard state, as that must happen only after the controlled animation
+ // has completed.
+ // We also must not request focus, as this triggers unwanted side effects.
+ showSoftInputInternal();
}
public void hideKeyboard() {
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 095cfa9..7d52cbb 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -563,7 +563,7 @@
mainRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(0);
workRecyclerView = (AllAppsRecyclerView) mViewPager.getChildAt(1);
mAH.get(AdapterHolder.MAIN).setup(mainRecyclerView, mPersonalMatcher);
- mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getMatcher());
+ mAH.get(AdapterHolder.WORK).setup(workRecyclerView, mWorkManager.getItemInfoMatcher());
workRecyclerView.setId(R.id.apps_list_view_work);
if (enableExpandingPauseWorkButton()
|| FeatureFlags.ENABLE_EXPANDING_PAUSE_WORK_BUTTON.get()) {
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 7baf7d3..bce38a3 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -52,13 +52,16 @@
public static final int VIEW_TYPE_WORK_EDU_CARD = 1 << 4;
public static final int VIEW_TYPE_WORK_DISABLED_CARD = 1 << 5;
-
- public static final int NEXT_ID = 6;
+ public static final int VIEW_TYPE_PRIVATE_SPACE_HEADER = 1 << 6;
+ public static final int NEXT_ID = 7;
// Common view type masks
public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON;
+ public static final int VIEW_TYPE_MASK_PRIVATE_SPACE_HEADER =
+ VIEW_TYPE_PRIVATE_SPACE_HEADER;
+
protected final SearchAdapterProvider<?> mAdapterProvider;
/**
@@ -196,6 +199,9 @@
case VIEW_TYPE_WORK_DISABLED_CARD:
return new ViewHolder(mLayoutInflater.inflate(
R.layout.work_apps_paused, parent, false));
+ case VIEW_TYPE_PRIVATE_SPACE_HEADER:
+ return new ViewHolder(mLayoutInflater.inflate(
+ R.layout.private_space_header, parent, false));
default:
if (mAdapterProvider.isViewSupported(viewType)) {
return mAdapterProvider.onCreateViewHolder(mLayoutInflater, parent, viewType);
@@ -223,6 +229,7 @@
}
break;
}
+ case VIEW_TYPE_PRIVATE_SPACE_HEADER:
case VIEW_TYPE_ALL_APPS_DIVIDER:
case VIEW_TYPE_WORK_DISABLED_CARD:
// nothing to do
diff --git a/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java b/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java
new file mode 100644
index 0000000..f7c9058
--- /dev/null
+++ b/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023 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.allapps;
+
+import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_ICON;
+import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.view.View;
+
+import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.R;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.views.ActivityContext;
+
+/**
+ * Decorator which changes the background color for Private Space Icon Rows in AllAppsContainer.
+ */
+public class PrivateAppsSectionDecorator extends RecyclerView.ItemDecoration {
+
+ private final Path mTmpPath = new Path();
+ private final RectF mTmpRect = new RectF();
+ private final Context mContext;
+ private final AlphabeticalAppsList<?> mAppsList;
+ private final PrivateProfileManager mPrivateProfileManager;
+ private final UserCache mUserCache;
+ private final Paint mPaint;
+
+ public PrivateAppsSectionDecorator(ActivityAllAppsContainerView<?> appsContainerView,
+ AlphabeticalAppsList<?> appsList,
+ PrivateProfileManager privateProfileManager) {
+ mAppsList = appsList;
+ mPrivateProfileManager = privateProfileManager;
+ mContext = appsContainerView.mActivityContext;
+ mUserCache = UserCache.getInstance(appsContainerView.mActivityContext);
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mPaint.setColor(ContextCompat.getColor(mContext,
+ R.color.material_color_surface_container_high));
+ }
+
+ /** Decorates Private Space Header and Icon Rows to give the shape of a container. */
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ mTmpPath.reset();
+ mTmpRect.setEmpty();
+ int numCol = ActivityContext.lookupContext(mContext).getDeviceProfile()
+ .numShownAllAppsColumns;
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ View view = parent.getChildAt(i);
+ int position = parent.getChildAdapterPosition(view);
+ BaseAllAppsAdapter.AdapterItem adapterItem = mAppsList.getAdapterItems().get(position);
+ // Rectangle that covers the bottom half of the PS Header View when Space is unlocked.
+ if (adapterItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER
+ && mPrivateProfileManager
+ .getCurrentState() == PrivateProfileManager.STATE_ENABLED) {
+ // We flatten the bottom corners of the rectangle, so that it merges with
+ // the private space app row decorator.
+ mTmpRect.set(
+ view.getLeft(),
+ view.getTop() + (float) (view.getBottom() - view.getTop()) / 2,
+ view.getRight(),
+ view.getBottom());
+ mTmpPath.addRect(mTmpRect, Path.Direction.CW);
+ c.drawPath(mTmpPath, mPaint);
+ } else if (adapterItem.viewType == VIEW_TYPE_ICON
+ && mUserCache.getUserInfo(adapterItem.itemInfo.user).isPrivate()
+ // No decoration for any private space app icon other than those at first row.
+ && adapterItem.rowAppIndex == 0) {
+ c.drawPath(getPrivateAppRowPath(parent, view, position, numCol), mPaint);
+ }
+ }
+ }
+
+ /** Returns the path to be decorated for Private Space App Row */
+ private Path getPrivateAppRowPath(RecyclerView parent, View iconView, int adapterPosition,
+ int numCol) {
+ // We always decorate the entire app row here.
+ // As the iconView just represents the first icon of the row, we get the right margin of
+ // our decorator using the parent view.
+ mTmpRect.set(iconView.getLeft(),
+ iconView.getTop(),
+ parent.getRight() - parent.getPaddingRight(),
+ iconView.getBottom());
+ // Decorates last app row with rounded bottom corners.
+ if (adapterPosition + numCol >= mAppsList.getAdapterItems().size()) {
+ int corner = mContext.getResources().getDimensionPixelSize(
+ R.dimen.ps_container_corner_radius);
+ float[] mCornersBot = new float[]{0, 0, 0, 0, corner, corner, corner, corner};
+ mTmpPath.addRoundRect(mTmpRect, mCornersBot, Path.Direction.CW);
+ } else {
+ // Decorate other rows as a plain rectangle
+ mTmpPath.addRect(mTmpRect, Path.Direction.CW);
+ }
+ return mTmpPath;
+ }
+}
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
new file mode 100644
index 0000000..ec01aee
--- /dev/null
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 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.allapps;
+
+import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN;
+import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
+import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.function.Predicate;
+
+/**
+ * Companion class for {@link ActivityAllAppsContainerView} to manage private space section related
+ * logic in the Personal tab.
+ */
+public class PrivateProfileManager extends UserProfileManager {
+
+ private static final String SAFETY_CENTER_INTENT = Intent.ACTION_SAFETY_CENTER;
+ private static final String PS_SETTINGS_FRAGMENT_KEY = ":settings:fragment_args_key";
+ private static final String PS_SETTINGS_FRAGMENT_VALUE = "AndroidPrivateSpace_personal";
+ private final ActivityAllAppsContainerView<?> mAllApps;
+ private final Predicate<UserHandle> mPrivateProfileMatcher;
+
+ public PrivateProfileManager(UserManager userManager,
+ UserCache userCache,
+ ActivityAllAppsContainerView allApps,
+ StatsLogManager statsLogManager) {
+ super(userManager, statsLogManager, userCache);
+ mAllApps = allApps;
+ mPrivateProfileMatcher = (user) -> userCache.getUserInfo(user).isPrivate();
+ }
+
+ /** Adds Private Space Header to the layout. */
+ public int addPrivateSpaceHeader(ArrayList<BaseAllAppsAdapter.AdapterItem> adapterItems) {
+ adapterItems.add(new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_PRIVATE_SPACE_HEADER));
+ mAllApps.mAH.get(MAIN).mAdapter.notifyItemInserted(adapterItems.size() - 1);
+ return adapterItems.size();
+ }
+
+ /** Disables quiet mode for Private Space User Profile. */
+ public void unlockPrivateProfile() {
+ // TODO (b/302666597): Log this event to WW.
+ enableQuietMode(false);
+ }
+
+ /** Enables quiet mode for Private Space User Profile. */
+ public void lockPrivateProfile() {
+ // TODO (b/302666597): Log this event to WW.
+ enableQuietMode(true);
+ }
+
+ /** Whether private profile should be hidden on Launcher. */
+ public boolean isPrivateSpaceHidden() {
+ // TODO (b/289223923): Update this when we are able to read PsSettingsFlag
+ // from SettingsProvider.
+ return false;
+ }
+
+ /** Resets the current state of Private Profile, w.r.t. to Launcher. */
+ public void reset() {
+ boolean isEnabled = !mAllApps.getAppsStore()
+ .hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED);
+ int updatedState = isEnabled ? STATE_ENABLED : STATE_DISABLED;
+ setCurrentState(updatedState);
+ }
+
+ /** Opens the Private Space Settings Entry Point. */
+ public void openPrivateSpaceSettings() {
+ // TODO (b/302666597): Log this event to WW.
+ Intent psSettingsIntent = new Intent(SAFETY_CENTER_INTENT);
+ psSettingsIntent.putExtra(PS_SETTINGS_FRAGMENT_KEY, PS_SETTINGS_FRAGMENT_VALUE);
+ mAllApps.getContext().startActivity(psSettingsIntent);
+ }
+
+ /**
+ * Whether Private Space Settings Entry Point should be made visible. */
+ public boolean isPrivateSpaceSettingsButtonVisible() {
+ Preconditions.assertNonUiThread();
+ Intent psSettingsIntent = new Intent(SAFETY_CENTER_INTENT);
+ psSettingsIntent.putExtra(PS_SETTINGS_FRAGMENT_KEY, PS_SETTINGS_FRAGMENT_VALUE);
+ ResolveInfo resolveInfo = mAllApps.getContext().getPackageManager()
+ .resolveActivity(psSettingsIntent, PackageManager.MATCH_SYSTEM_ONLY);
+ return resolveInfo != null;
+ }
+
+ /** Posts quiet mode enable/disable call for private profile. */
+ private void enableQuietMode(boolean enable) {
+ setQuietMode(enable);
+ }
+
+ @Override
+ public Predicate<UserHandle> getUserMatcher() {
+ return mPrivateProfileMatcher;
+ }
+}
diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
new file mode 100644
index 0000000..9420b4c
--- /dev/null
+++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 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.allapps;
+
+import static com.android.launcher3.allapps.PrivateProfileManager.STATE_DISABLED;
+import static com.android.launcher3.allapps.PrivateProfileManager.STATE_ENABLED;
+import static com.android.launcher3.allapps.PrivateProfileManager.STATE_TRANSITION;
+
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.UserProfileManager.UserProfileState;
+
+/**
+ * Controller which returns views to be added to Private Space Header based upon
+ * {@link UserProfileState}
+ */
+public class PrivateSpaceHeaderViewController {
+ private final PrivateProfileManager mPrivateProfileManager;
+
+ public PrivateSpaceHeaderViewController(PrivateProfileManager privateProfileManager) {
+ this.mPrivateProfileManager = privateProfileManager;
+ }
+
+ /** Add Private Space Header view elements based upon {@link UserProfileState} */
+ public void addPrivateSpaceHeaderViewElements(RelativeLayout parent) {
+ //Add quietMode image and action for lock/unlock button
+ ImageButton quietModeButton = parent.findViewById(R.id.ps_lock_unlock_button);
+ assert quietModeButton != null;
+ addQuietModeButton(quietModeButton);
+
+ //Add image and action for private space settings button
+ ImageButton settingsButton = parent.findViewById(R.id.ps_settings_button);
+ assert settingsButton != null;
+ addPrivateSpaceSettingsButton(settingsButton);
+
+ //Add image for private space transitioning view
+ ImageView transitionView = parent.findViewById(R.id.ps_transition_image);
+ assert transitionView != null;
+ addTransitionImage(transitionView);
+ }
+
+ private void addQuietModeButton(ImageButton quietModeButton) {
+ switch (mPrivateProfileManager.getCurrentState()) {
+ case STATE_ENABLED -> {
+ quietModeButton.setVisibility(View.VISIBLE);
+ quietModeButton.setImageResource(R.drawable.bg_ps_lock_button);
+ quietModeButton.setOnClickListener(
+ view -> mPrivateProfileManager.lockPrivateProfile());
+ }
+ case STATE_DISABLED -> {
+ quietModeButton.setVisibility(View.VISIBLE);
+ quietModeButton.setImageResource(R.drawable.bg_ps_unlock_button);
+ quietModeButton.setOnClickListener(
+ view -> mPrivateProfileManager.unlockPrivateProfile());
+ }
+ default -> quietModeButton.setVisibility(View.GONE);
+ }
+ }
+
+ private void addPrivateSpaceSettingsButton(ImageButton settingsButton) {
+ if (mPrivateProfileManager.getCurrentState() == STATE_ENABLED
+ && mPrivateProfileManager.isPrivateSpaceSettingsButtonVisible()) {
+ settingsButton.setVisibility(View.VISIBLE);
+ settingsButton.setOnClickListener(view ->
+ mPrivateProfileManager.openPrivateSpaceSettings());
+ } else {
+ settingsButton.setVisibility(View.GONE);
+ }
+ }
+
+ private void addTransitionImage(ImageView transitionImage) {
+ if (mPrivateProfileManager.getCurrentState() == STATE_TRANSITION) {
+ transitionImage.setVisibility(View.VISIBLE);
+ } else {
+ transitionImage.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/src/com/android/launcher3/allapps/UserProfileManager.java b/src/com/android/launcher3/allapps/UserProfileManager.java
new file mode 100644
index 0000000..0261010
--- /dev/null
+++ b/src/com/android/launcher3/allapps/UserProfileManager.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2023 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.allapps;
+
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.pm.UserCache;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.function.Predicate;
+
+/**
+ * A Generic User Profile Manager which abstract outs the common functionality required
+ * by user-profiles supported by Launcher
+ * <p>
+ * Concrete impls are
+ * {@link WorkProfileManager} which manages work profile state
+ * {@link PrivateProfileManager} which manages private profile state.
+ */
+public abstract class UserProfileManager {
+ public static final int STATE_ENABLED = 1;
+ public static final int STATE_DISABLED = 2;
+ public static final int STATE_TRANSITION = 3;
+
+ @IntDef(value = {
+ STATE_ENABLED,
+ STATE_DISABLED,
+ STATE_TRANSITION
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface UserProfileState { }
+
+ @UserProfileState
+ private int mCurrentState;
+
+ private final UserManager mUserManager;
+ private final StatsLogManager mStatsLogManager;
+ private final UserCache mUserCache;
+
+ protected UserProfileManager(UserManager userManager,
+ StatsLogManager statsLogManager,
+ UserCache userCache) {
+ mUserManager = userManager;
+ mStatsLogManager = statsLogManager;
+ mUserCache = userCache;
+ }
+
+ /** Sets quiet mode as enabled/disabled for the profile type. */
+ protected void setQuietMode(boolean enabled) {
+ if (Utilities.ATLEAST_P) {
+ UI_HELPER_EXECUTOR.post(() -> {
+ mUserCache.getUserProfiles()
+ .stream()
+ .filter(getUserMatcher())
+ .findFirst()
+ .ifPresent(userHandle ->
+ mUserManager.requestQuietModeEnabled(enabled, userHandle));
+ });
+ }
+ }
+
+ /** Sets current state for the profile type. */
+ protected void setCurrentState(int state) {
+ mCurrentState = state;
+ }
+
+ /** Returns current state for the profile type. */
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public int getCurrentState() {
+ return mCurrentState;
+ }
+
+ /** Logs Event to StatsLogManager. */
+ protected void logEvents(StatsLogManager.EventEnum event) {
+ mStatsLogManager.logger().log(event);
+ }
+
+ /** Returns the matcher corresponding to profile type. */
+ protected abstract Predicate<UserHandle> getUserMatcher();
+
+ /** Returns the matcher corresponding to the profile type associated with ItemInfo. */
+ protected Predicate<ItemInfo> getItemInfoMatcher() {
+ return itemInfo -> itemInfo != null && getUserMatcher().test(itemInfo.user);
+ }
+}
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index 61c3d3f..c430a36 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -26,18 +26,14 @@
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;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import android.os.Build;
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.Flags;
@@ -46,12 +42,9 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
import com.android.launcher3.logging.StatsLogManager;
-import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.function.Predicate;
import java.util.stream.Stream;
@@ -59,62 +52,29 @@
/**
* Companion class for {@link ActivityAllAppsContainerView} to manage work tab and personal tab
* related
- * logic based on {@link WorkProfileState}?
+ * logic based on {@link UserProfileState}?
*/
-public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActivePageChangedListener {
+public class WorkProfileManager extends UserProfileManager
+ implements PersonalWorkSlidingTabStrip.OnActivePageChangedListener {
private static final String TAG = "WorkProfileManager";
-
- public static final int STATE_ENABLED = 1;
- public static final int STATE_DISABLED = 2;
- public static final int STATE_TRANSITION = 3;
-
- /**
- * Work profile manager states
- */
- @IntDef(value = {
- STATE_ENABLED,
- STATE_DISABLED,
- STATE_TRANSITION
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface WorkProfileState { }
-
- private final UserManager mUserManager;
private final ActivityAllAppsContainerView<?> mAllApps;
- private final Predicate<ItemInfo> mMatcher;
- private final StatsLogManager mStatsLogManager;
-
private WorkModeSwitch mWorkModeSwitch;
-
- private final UserCache mUserCache;
-
- @WorkProfileState
- private int mCurrentState;
+ private final Predicate<UserHandle> mWorkProfileMatcher;
public WorkProfileManager(
UserManager userManager, ActivityAllAppsContainerView allApps,
StatsLogManager statsLogManager, UserCache userCache) {
- mUserManager = userManager;
+ super(userManager, statsLogManager, userCache);
mAllApps = allApps;
- mStatsLogManager = statsLogManager;
- mUserCache = userCache;
- mMatcher = info -> info != null && mUserCache.getUserInfo(info.user).isWork();
+ mWorkProfileMatcher = (user) -> userCache.getUserInfo(user).isWork();
}
/**
* Posts quite mode enable/disable call for work profile user
*/
- @RequiresApi(Build.VERSION_CODES.P)
public void setWorkProfileEnabled(boolean enabled) {
- updateCurrentState(STATE_TRANSITION);
- UI_HELPER_EXECUTOR.post(() -> {
- for (UserHandle userProfile : mUserCache.getUserProfiles()) {
- if (mUserCache.getUserInfo(userProfile).isWork()) {
- mUserManager.requestQuietModeEnabled(!enabled, userProfile);
- break;
- }
- }
- });
+ setCurrentState(STATE_TRANSITION);
+ setQuietMode(!enabled);
}
@Override
@@ -126,7 +86,7 @@
if (mWorkModeSwitch != null) {
if (page == MAIN || page == SEARCH) {
mWorkModeSwitch.animateVisibility(false);
- } else if (page == WORK && mCurrentState == STATE_ENABLED) {
+ } else if (page == WORK && getCurrentState() == STATE_ENABLED) {
mWorkModeSwitch.animateVisibility(true);
}
}
@@ -151,17 +111,17 @@
}
}
- private void updateCurrentState(@WorkProfileState int currentState) {
- mCurrentState = currentState;
+ private void updateCurrentState(@UserProfileState int currentState) {
+ setCurrentState(currentState);
if (getAH() != null) {
getAH().mAppsList.updateAdapterItems();
}
if (mWorkModeSwitch != null) {
updateWorkFAB(mAllApps.getCurrentPage());
}
- if (mCurrentState == STATE_ENABLED) {
+ if (getCurrentState() == STATE_ENABLED) {
attachWorkModeSwitch();
- } else if (mCurrentState == STATE_DISABLED) {
+ } else if (getCurrentState() == STATE_DISABLED) {
detachWorkModeSwitch();
}
}
@@ -201,10 +161,6 @@
mWorkModeSwitch = null;
}
- public Predicate<ItemInfo> getMatcher() {
- return mMatcher;
- }
-
@Nullable
public WorkModeSwitch getWorkModeSwitch() {
return mWorkModeSwitch;
@@ -214,29 +170,25 @@
return mAllApps.mAH.get(WORK);
}
- public int getCurrentState() {
- return mCurrentState;
- }
-
/**
* returns whether or not work apps should be visible in work tab.
*/
public boolean shouldShowWorkApps() {
- return mCurrentState != WorkProfileManager.STATE_DISABLED;
+ return getCurrentState() != WorkProfileManager.STATE_DISABLED;
}
public boolean hasWorkApps() {
- return Stream.of(mAllApps.getAppsStore().getApps()).anyMatch(mMatcher);
+ return Stream.of(mAllApps.getAppsStore().getApps()).anyMatch(getItemInfoMatcher());
}
/**
* Adds work profile specific adapter items to adapterItems and returns number of items added
*/
public int addWorkItems(ArrayList<AdapterItem> adapterItems) {
- if (mCurrentState == WorkProfileManager.STATE_DISABLED) {
+ if (getCurrentState() == WorkProfileManager.STATE_DISABLED) {
//add disabled card here.
adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_DISABLED_CARD));
- } else if (mCurrentState == WorkProfileManager.STATE_ENABLED && !isEduSeen()) {
+ } else if (getCurrentState() == WorkProfileManager.STATE_ENABLED && !isEduSeen()) {
adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_EDU_CARD));
}
return adapterItems.size();
@@ -247,8 +199,9 @@
}
private void onWorkFabClicked(View view) {
- if (Utilities.ATLEAST_P && mCurrentState == STATE_ENABLED && mWorkModeSwitch.isEnabled()) {
- mStatsLogManager.logger().log(LAUNCHER_TURN_OFF_WORK_APPS_TAP);
+ if (Utilities.ATLEAST_P && getCurrentState() == STATE_ENABLED
+ && mWorkModeSwitch.isEnabled()) {
+ logEvents(LAUNCHER_TURN_OFF_WORK_APPS_TAP);
setWorkProfileEnabled(false);
}
}
@@ -279,4 +232,9 @@
}
};
}
+
+ @Override
+ public Predicate<UserHandle> getUserMatcher() {
+ return mWorkProfileMatcher;
+ }
}
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index ecbc7a9..4427a49 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -160,7 +160,7 @@
* Focuses the search field to handle key events.
*/
public void focusSearchField() {
- mInput.showKeyboard(true /* shouldFocus */);
+ mInput.showKeyboard();
}
/**
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index c2d9e02..1f07352 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -50,6 +50,7 @@
import android.widget.FrameLayout;
import android.widget.ImageView;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringAnimation;
@@ -614,7 +615,7 @@
/**
* Removes any stray DragView from the DragLayer.
*/
- public static void removeAllViews(ActivityContext activity) {
+ public static void removeAllViews(@NonNull ActivityContext activity) {
BaseDragLayer dragLayer = activity.getDragLayer();
// Iterate in reverse order. DragView is added later to the dragLayer,
// and will be one of the last views.
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 57e1641..8bf7ec2 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -536,7 +536,7 @@
mFolderName.selectAll();
}
}
- mFolderName.showKeyboard(true /* shouldFocus */);
+ mFolderName.showKeyboard();
mFolderName.displayCompletions(
Stream.of(mInfo.suggestedFolderNames.getLabels())
.filter(Objects::nonNull)
diff --git a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
new file mode 100644
index 0000000..bfa9241
--- /dev/null
+++ b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 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.allapps;
+
+import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED;
+import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED;
+import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.UserIconInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class PrivateProfileManagerTest {
+
+ private static final UserHandle MAIN_HANDLE = Process.myUserHandle();
+ private static final UserHandle PRIVATE_HANDLE = new UserHandle(11);
+ private static final UserIconInfo MAIN_ICON_INFO =
+ new UserIconInfo(MAIN_HANDLE, UserIconInfo.TYPE_MAIN);
+ private static final UserIconInfo PRIVATE_ICON_INFO =
+ new UserIconInfo(PRIVATE_HANDLE, UserIconInfo.TYPE_PRIVATE);
+ private static final String SAFETY_CENTER_INTENT = Intent.ACTION_SAFETY_CENTER;
+ private static final String PS_SETTINGS_FRAGMENT_KEY = ":settings:fragment_args_key";
+ private static final String PS_SETTINGS_FRAGMENT_VALUE = "AndroidPrivateSpace_personal";
+
+ private PrivateProfileManager mPrivateProfileManager;
+ @Mock
+ private ActivityAllAppsContainerView mActivityAllAppsContainerView;
+ @Mock
+ private StatsLogManager mStatsLogManager;
+ @Mock
+ private UserCache mUserCache;
+ @Mock
+ private UserManager mUserManager;
+ @Mock
+ private Context mContext;
+ @Mock
+ private AllAppsStore mAllAppsStore;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mUserCache.getUserProfiles())
+ .thenReturn(Arrays.asList(MAIN_HANDLE, PRIVATE_HANDLE));
+ when(mUserCache.getUserInfo(Process.myUserHandle())).thenReturn(MAIN_ICON_INFO);
+ when(mUserCache.getUserInfo(PRIVATE_HANDLE)).thenReturn(PRIVATE_ICON_INFO);
+ when(mActivityAllAppsContainerView.getContext()).thenReturn(mContext);
+ when(mActivityAllAppsContainerView.getAppsStore()).thenReturn(mAllAppsStore);
+ mPrivateProfileManager = new PrivateProfileManager(mUserManager, mUserCache,
+ mActivityAllAppsContainerView, mStatsLogManager);
+ }
+
+ @Test
+ public void lockPrivateProfile_requestsQuietModeAsTrue() throws Exception {
+ when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)).thenReturn(false);
+
+ mPrivateProfileManager.lockPrivateProfile();
+
+ awaitTasksCompleted();
+ Mockito.verify(mUserManager).requestQuietModeEnabled(true, PRIVATE_HANDLE);
+ }
+
+ @Test
+ public void unlockPrivateProfile_requestsQuietModeAsFalse() throws Exception {
+ when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)).thenReturn(true);
+
+ mPrivateProfileManager.unlockPrivateProfile();
+
+ awaitTasksCompleted();
+ Mockito.verify(mUserManager).requestQuietModeEnabled(false, PRIVATE_HANDLE);
+ }
+
+ @Test
+ public void quietModeFlagPresent_privateSpaceIsResetToDisabled() {
+ when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED))
+ .thenReturn(false, true);
+
+ // In first call the state should be disabled.
+ mPrivateProfileManager.reset();
+ assertEquals(STATE_ENABLED, mPrivateProfileManager.getCurrentState());
+
+ // In the next call the state should be disabled.
+ mPrivateProfileManager.reset();
+ assertEquals(STATE_DISABLED, mPrivateProfileManager.getCurrentState());
+ }
+
+ @Test
+ public void openPrivateSpaceSettings_triggersSecurityAndPrivacyIntent() {
+ Intent expectedIntent = new Intent(SAFETY_CENTER_INTENT);
+ expectedIntent.putExtra(PS_SETTINGS_FRAGMENT_KEY, PS_SETTINGS_FRAGMENT_VALUE);
+ ArgumentCaptor<Intent> acIntent = ArgumentCaptor.forClass(Intent.class);
+
+ mPrivateProfileManager.openPrivateSpaceSettings();
+
+ Mockito.verify(mContext).startActivity(acIntent.capture());
+ Intent actualIntent = acIntent.getValue();
+ assertEquals(expectedIntent.getAction(), actualIntent.getAction());
+ assertEquals(expectedIntent.getStringExtra(PS_SETTINGS_FRAGMENT_KEY),
+ actualIntent.getStringExtra(PS_SETTINGS_FRAGMENT_KEY));
+ }
+
+ private static void awaitTasksCompleted() throws Exception {
+ UI_HELPER_EXECUTOR.submit(() -> null).get();
+ }
+}
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java
new file mode 100644
index 0000000..87adaa1
--- /dev/null
+++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewControllerTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2023 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.allapps;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED;
+import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED;
+import static com.android.launcher3.allapps.UserProfileManager.STATE_TRANSITION;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.ActivityContextWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PrivateSpaceHeaderViewControllerTest {
+
+ private static final int CONTAINER_HEADER_ELEMENT_COUNT = 1;
+ private static final int LOCK_UNLOCK_BUTTON_COUNT = 1;
+ private static final int PS_SETTINGS_BUTTON_COUNT_VISIBLE = 1;
+ private static final int PS_SETTINGS_BUTTON_COUNT_INVISIBLE = 0;
+ private static final int PS_TRANSITION_IMAGE_COUNT = 1;
+
+ private Context mContext;
+ private LayoutInflater mLayoutInflater;
+ private PrivateSpaceHeaderViewController mPsHeaderViewController;
+ private RelativeLayout mPsHeaderLayout;
+ @Mock
+ private PrivateProfileManager mPrivateProfileManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = new ActivityContextWrapper(getApplicationContext());
+ mLayoutInflater = LayoutInflater.from(getApplicationContext());
+ mPsHeaderViewController = new PrivateSpaceHeaderViewController(mPrivateProfileManager);
+ mPsHeaderLayout = (RelativeLayout) mLayoutInflater.inflate(R.layout.private_space_header,
+ null);
+ }
+
+ @Test
+ public void privateProfileDisabled_psHeaderContainsLockedView() {
+ Bitmap unlockButton = getBitmap(mContext.getDrawable(R.drawable.bg_ps_unlock_button));
+ when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_DISABLED);
+
+ mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout);
+
+ int totalContainerHeaderView = 0;
+ int totalLockUnlockButtonView = 0;
+ for (int i = 0; i < mPsHeaderLayout.getChildCount(); i++) {
+ View view = mPsHeaderLayout.getChildAt(i);
+ if (view.getId() == R.id.ps_container_header) {
+ totalContainerHeaderView += 1;
+ assertEquals(View.VISIBLE, view.getVisibility());
+ } else if (view.getId() == R.id.ps_lock_unlock_button
+ && view instanceof ImageView imageView) {
+ totalLockUnlockButtonView += 1;
+ assertEquals(View.VISIBLE, view.getVisibility());
+ getBitmap(imageView.getDrawable()).sameAs(unlockButton);
+ } else {
+ assertEquals(View.GONE, view.getVisibility());
+ }
+ }
+ assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView);
+ assertEquals(LOCK_UNLOCK_BUTTON_COUNT, totalLockUnlockButtonView);
+ }
+
+ @Test
+ public void privateProfileEnabled_psHeaderContainsUnlockedView() {
+ Bitmap lockImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_lock_button));
+ Bitmap settingsImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_settings_button));
+ when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED);
+ when(mPrivateProfileManager.isPrivateSpaceSettingsButtonVisible()).thenReturn(true);
+
+ mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout);
+
+ int totalContainerHeaderView = 0;
+ int totalLockUnlockButtonView = 0;
+ int totalSettingsImageView = 0;
+ for (int i = 0; i < mPsHeaderLayout.getChildCount(); i++) {
+ View view = mPsHeaderLayout.getChildAt(i);
+ if (view.getId() == R.id.ps_container_header) {
+ totalContainerHeaderView += 1;
+ assertEquals(View.VISIBLE, view.getVisibility());
+ } else if (view.getId() == R.id.ps_lock_unlock_button
+ && view instanceof ImageView imageView) {
+ totalLockUnlockButtonView += 1;
+ assertEquals(View.VISIBLE, view.getVisibility());
+ getBitmap(imageView.getDrawable()).sameAs(lockImage);
+ } else if (view.getId() == R.id.ps_settings_button
+ && view instanceof ImageView imageView) {
+ totalSettingsImageView += 1;
+ assertEquals(View.VISIBLE, view.getVisibility());
+ getBitmap(imageView.getDrawable()).sameAs(settingsImage);
+ } else {
+ assertEquals(View.GONE, view.getVisibility());
+ }
+ }
+ assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView);
+ assertEquals(LOCK_UNLOCK_BUTTON_COUNT, totalLockUnlockButtonView);
+ assertEquals(PS_SETTINGS_BUTTON_COUNT_VISIBLE, totalSettingsImageView);
+ }
+
+ @Test
+ public void privateProfileEnabledAndNoSettingsIntent_psHeaderContainsUnlockedView() {
+ Bitmap lockImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_lock_button));
+ when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_ENABLED);
+ when(mPrivateProfileManager.isPrivateSpaceSettingsButtonVisible()).thenReturn(false);
+
+ mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout);
+
+ int totalContainerHeaderView = 0;
+ int totalLockUnlockButtonView = 0;
+ int totalSettingsImageView = 0;
+ for (int i = 0; i < mPsHeaderLayout.getChildCount(); i++) {
+ View view = mPsHeaderLayout.getChildAt(i);
+ if (view.getId() == R.id.ps_container_header) {
+ totalContainerHeaderView += 1;
+ assertEquals(View.VISIBLE, view.getVisibility());
+ } else if (view.getId() == R.id.ps_lock_unlock_button
+ && view instanceof ImageView imageView) {
+ totalLockUnlockButtonView += 1;
+ assertEquals(View.VISIBLE, view.getVisibility());
+ getBitmap(imageView.getDrawable()).sameAs(lockImage);
+ } else {
+ assertEquals(View.GONE, view.getVisibility());
+ }
+ }
+ assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView);
+ assertEquals(LOCK_UNLOCK_BUTTON_COUNT, totalLockUnlockButtonView);
+ assertEquals(PS_SETTINGS_BUTTON_COUNT_INVISIBLE, totalSettingsImageView);
+ }
+
+ @Test
+ public void privateProfileTransitioning_psHeaderContainsTransitionView() {
+ Bitmap transitionImage = getBitmap(mContext.getDrawable(R.drawable.bg_ps_transition_image));
+ when(mPrivateProfileManager.getCurrentState()).thenReturn(STATE_TRANSITION);
+
+ mPsHeaderViewController.addPrivateSpaceHeaderViewElements(mPsHeaderLayout);
+
+ int totalContainerHeaderView = 0;
+ int totalLockUnlockButtonView = 0;
+ for (int i = 0; i < mPsHeaderLayout.getChildCount(); i++) {
+ View view = mPsHeaderLayout.getChildAt(i);
+ if (view.getId() == R.id.ps_container_header) {
+ totalContainerHeaderView += 1;
+ assertEquals(View.VISIBLE, view.getVisibility());
+ } else if (view.getId() == R.id.ps_transition_image
+ && view instanceof ImageView imageView) {
+ totalLockUnlockButtonView += 1;
+ assertEquals(View.VISIBLE, view.getVisibility());
+ getBitmap(imageView.getDrawable()).sameAs(transitionImage);
+ } else {
+ assertEquals(View.GONE, view.getVisibility());
+ }
+ }
+ assertEquals(CONTAINER_HEADER_ELEMENT_COUNT, totalContainerHeaderView);
+ assertEquals(PS_TRANSITION_IMAGE_COUNT, totalLockUnlockButtonView);
+ }
+
+ private Bitmap getBitmap(Drawable drawable) {
+ Bitmap result;
+ if (drawable instanceof BitmapDrawable) {
+ result = ((BitmapDrawable) drawable).getBitmap();
+ } else {
+ int width = drawable.getIntrinsicWidth();
+ int height = drawable.getIntrinsicHeight();
+ // Some drawables have no intrinsic width - e.g. solid colours.
+ if (width <= 0) {
+ width = 1;
+ }
+ if (height <= 0) {
+ height = 1;
+ }
+
+ result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(result);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ }
+ return result;
+ }
+}