Merge "Revert change to rotateBounds" into main
diff --git a/quickstep/res/values/ids.xml b/quickstep/res/values/ids.xml
new file mode 100644
index 0000000..3091d9e
--- /dev/null
+++ b/quickstep/res/values/ids.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 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.
+-->
+<resources>
+ <!-- Used for A11y actions for bubble bar -->
+ <item type="id" name="action_move_left" />
+ <item type="id" name="action_move_right" />
+ <item type="id" name="action_dismiss_all" />
+</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 340d25b..98a2783 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -342,4 +342,10 @@
<string name="bubble_bar_bubble_description"><xliff:g id="notification_title" example="some title">%1$s</xliff:g> from <xliff:g id="app_name" example="YouTube">%2$s</xliff:g></string>
<!-- Content description for bubble bar when it has multiple bubbles. [CHAR_LIMIT=NONE] -->
<string name="bubble_bar_description_multiple_bubbles"><xliff:g id="bubble_bar_bubble_description" example="some title from YouTube">%1$s</xliff:g> and <xliff:g id="bubble_count" example="4">%2$d</xliff:g> more</string>
+ <!-- Action in accessibility menu to move the bubble bar to the left side of the screen. [CHAR_LIMIT=30] -->
+ <string name="bubble_bar_action_move_left">Move left</string>
+ <!-- Action in accessibility menu to move the bubble bar to the right side of the screen. [CHAR_LIMIT=30] -->
+ <string name="bubble_bar_action_move_right">Move right</string>
+ <!-- Action in accessibility menu to dismiss all bubbles. [CHAR_LIMIT=30] -->
+ <string name="bubble_bar_action_dismiss_all">Dismiss all</string>
</resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 4794dfd..fab7975 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -30,6 +30,7 @@
import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.os.Bundle;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.LayoutDirection;
@@ -38,6 +39,7 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import androidx.dynamicanimation.animation.SpringForce;
@@ -367,6 +369,47 @@
}
}
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+ // Always show only expand action as the menu is only for collapsed bubble bar
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_dismiss_all,
+ getResources().getString(R.string.bubble_bar_action_dismiss_all)));
+ if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right,
+ getResources().getString(R.string.bubble_bar_action_move_right)));
+ } else {
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left,
+ getResources().getString(R.string.bubble_bar_action_move_left)));
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityActionInternal(int action,
+ @androidx.annotation.Nullable Bundle arguments) {
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+ if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
+ mController.expandBubbleBar();
+ return true;
+ }
+ if (action == R.id.action_dismiss_all) {
+ mController.dismissBubbleBar();
+ return true;
+ }
+ if (action == R.id.action_move_left) {
+ mController.updateBubbleBarLocation(BubbleBarLocation.LEFT);
+ return true;
+ }
+ if (action == R.id.action_move_right) {
+ mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT);
+ return true;
+ }
+ return false;
+ }
+
@SuppressLint("RtlHardcoded")
private void onBubbleBarLocationChanged() {
final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
@@ -758,7 +801,6 @@
listener);
}
- // TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
@@ -1382,5 +1424,14 @@
/** Notifies the controller that the bubble bar was touched while it was animating. */
void onBubbleBarTouchedWhileAnimating();
+
+ /** Requests the controller to expand bubble bar */
+ void expandBubbleBar();
+
+ /** Requests the controller to dismiss the bubble bar */
+ void dismissBubbleBar();
+
+ /** Requests the controller to update bubble bar location to the given value */
+ void updateBubbleBarLocation(BubbleBarLocation location);
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 2311d42..74a673b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -117,7 +117,7 @@
dp -> onBubbleBarConfigurationChanged(/* animate= */ true));
mBubbleBarScale.updateValue(1f);
mBubbleClickListener = v -> onBubbleClicked((BubbleView) v);
- mBubbleBarClickListener = v -> onBubbleBarClicked();
+ mBubbleBarClickListener = v -> expandBubbleBar();
mBubbleDragController.setupBubbleBarView(mBarView);
mBarView.setOnClickListener(mBubbleBarClickListener);
mBarView.addOnLayoutChangeListener(
@@ -137,6 +137,21 @@
public void onBubbleBarTouchedWhileAnimating() {
BubbleBarViewController.this.onBubbleBarTouchedWhileAnimating();
}
+
+ @Override
+ public void expandBubbleBar() {
+ BubbleBarViewController.this.expandBubbleBar();
+ }
+
+ @Override
+ public void dismissBubbleBar() {
+ onDismissAllBubbles();
+ }
+
+ @Override
+ public void updateBubbleBarLocation(BubbleBarLocation location) {
+ mBubbleBarController.updateBubbleBarLocation(location);
+ }
});
}
@@ -162,7 +177,7 @@
mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY());
}
- private void onBubbleBarClicked() {
+ private void expandBubbleBar() {
if (mShouldShowEducation) {
mShouldShowEducation = false;
// Get the bubble bar bounds on screen
@@ -609,17 +624,17 @@
}
/**
- * Called when bubble was dragged into the dismiss target. Notifies System
+ * Called when given bubble was dismissed. Notifies SystemUI
* @param bubble dismissed bubble item
*/
- public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
+ public void onDismissBubble(@NonNull BubbleBarItem bubble) {
mSystemUiProxy.dragBubbleToDismiss(bubble.getKey(), mTimeSource.currentTimeMillis());
}
/**
- * Called when bubble stack was dragged into the dismiss target
+ * Called when bubble stack was dismissed
*/
- public void onDismissAllBubblesWhileDragging() {
+ public void onDismissAllBubbles() {
mSystemUiProxy.removeAllBubbles();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
index a6096e2..6a63da8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -143,10 +143,10 @@
if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
if (bubbleView.getBubble() != null) {
- mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
+ mBubbleBarViewController.onDismissBubble(bubbleView.getBubble());
}
} else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
- mBubbleBarViewController.onDismissAllBubblesWhileDragging();
+ mBubbleBarViewController.onDismissAllBubbles();
}
}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index cd62265..f902284 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -33,6 +33,7 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
@@ -416,7 +417,8 @@
| SYSUI_STATE_QUICK_SETTINGS_EXPANDED
| SYSUI_STATE_MAGNIFICATION_OVERLAP
| SYSUI_STATE_DEVICE_DREAMING
- | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
+ | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION
+ | SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
return (gestureDisablingStates & mSystemUiStateFlags) == 0 && homeOrOverviewEnabled;
}
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 7a5a714..3a6d5c0 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -24,6 +24,7 @@
import com.android.quickstep.recents.data.TasksRepository
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
@@ -162,6 +163,8 @@
)
}
GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
+ SysUiStatusNavFlagsUseCase::class.java ->
+ SysUiStatusNavFlagsUseCase(taskRepository = inject())
GetThumbnailPositionUseCase::class.java ->
GetThumbnailPositionUseCase(
deviceProfileRepository = inject(),
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt
new file mode 100644
index 0000000..1d19c7d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 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.recents.usecase
+
+import android.view.WindowInsetsController
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
+import com.android.quickstep.recents.data.RecentTasksRepository
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+
+/** UseCase to calculate flags for status bar and navigation bar */
+class SysUiStatusNavFlagsUseCase(private val taskRepository: RecentTasksRepository) {
+ fun getSysUiStatusNavFlags(taskId: Int): Int {
+ val thumbnailData =
+ runBlocking { taskRepository.getThumbnailById(taskId).firstOrNull() } ?: return 0
+
+ val thumbnailAppearance = thumbnailData.appearance
+ var flags = 0
+ flags =
+ flags or
+ if (
+ thumbnailAppearance and WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS != 0
+ )
+ FLAG_LIGHT_STATUS
+ else FLAG_DARK_STATUS
+ flags =
+ flags or
+ if (
+ thumbnailAppearance and
+ WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS != 0
+ )
+ FLAG_LIGHT_NAV
+ else FLAG_DARK_NAV
+ return flags
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
new file mode 100644
index 0000000..8b8bc3e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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.recents.viewmodel
+
+import android.graphics.Bitmap
+import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
+
+class TaskContainerViewModel(
+ private val sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase,
+ private val getThumbnailUseCase: GetThumbnailUseCase
+) {
+ fun getThumbnail(taskId: Int): Bitmap? = getThumbnailUseCase.run(taskId)
+
+ fun getSysUiStatusNavFlags(taskId: Int) =
+ sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(taskId)
+}
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index 8e3d44f..31aca03 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -17,18 +17,28 @@
package com.android.quickstep.util;
import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import android.animation.AnimatorSet;
+import android.annotation.NonNull;
import android.os.Bundle;
import android.os.IRemoteCallback;
import android.view.animation.Interpolator;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.util.RunnableList;
+import com.android.quickstep.views.RecentsViewContainer;
/**
* Utility class containing methods to help manage animations, interpolators, and timings.
*/
public class AnimUtils {
+ private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
+
/**
* Fetches device-specific timings for the Overview > Split animation
* (splitscreen initiated from Overview).
@@ -59,6 +69,33 @@
}
/**
+ * Synchronizes the timing for the split dismiss animation to the current transition to
+ * NORMAL (launcher home/workspace)
+ */
+ public static void goToNormalStateWithSplitDismissal(@NonNull StateManager stateManager,
+ @NonNull RecentsViewContainer container,
+ @NonNull StatsLogManager.LauncherEvent exitReason,
+ @NonNull SplitAnimationController animationController) {
+ StateAnimationConfig config = new StateAnimationConfig();
+ BaseState startState = stateManager.getState();
+ long duration = startState.getTransitionDuration(container.asContext(),
+ false /*isToState*/);
+ if (duration == 0) {
+ // Case where we're in contextual on workspace (NORMAL), which by default has 0
+ // transition duration
+ duration = DURATION_DEFAULT_SPLIT_DISMISS;
+ }
+ config.duration = duration;
+ AnimatorSet stateAnim = stateManager.createAtomicAnimation(
+ startState, NORMAL, config);
+ AnimatorSet dismissAnim = animationController
+ .createPlaceholderDismissAnim(container, exitReason, duration);
+ stateAnim.play(dismissAnim);
+ stateManager.setCurrentAnimation(stateAnim, NORMAL);
+ stateAnim.start();
+ }
+
+ /**
* Returns a IRemoteCallback which completes the provided list as a result
*/
public static IRemoteCallback completeRunnableListCallback(RunnableList list) {
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
deleted file mode 100644
index bb4a7ec..0000000
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.views.ActivityContext;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.RecentsOrientedState;
-
-/**
- * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
- * when the drawable changes.
- */
-public class IconView extends View implements TaskViewIcon {
- private static final int NUM_ALPHA_CHANNELS = 2;
- private static final int INDEX_CONTENT_ALPHA = 0;
- private static final int INDEX_MODAL_ALPHA = 1;
-
- private final MultiValueAlpha mMultiValueAlpha;
-
- @Nullable
- private Drawable mDrawable;
- private int mDrawableWidth, mDrawableHeight;
-
- public IconView(Context context) {
- this(context, null);
- }
-
- public IconView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public IconView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public IconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
- mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
- }
-
- /**
- * Sets a {@link Drawable} to be displayed.
- */
- @Override
- public void setDrawable(@Nullable Drawable d) {
- if (mDrawable != null) {
- mDrawable.setCallback(null);
- }
- mDrawable = d;
- if (mDrawable != null) {
- mDrawable.setCallback(this);
- setDrawableSizeInternal(getWidth(), getHeight());
- }
- invalidate();
- }
-
- /**
- * Sets the size of the icon drawable.
- */
- @Override
- public void setDrawableSize(int iconWidth, int iconHeight) {
- mDrawableWidth = iconWidth;
- mDrawableHeight = iconHeight;
- if (mDrawable != null) {
- setDrawableSizeInternal(getWidth(), getHeight());
- }
- }
-
- private void setDrawableSizeInternal(int selfWidth, int selfHeight) {
- Rect selfRect = new Rect(0, 0, selfWidth, selfHeight);
- Rect drawableRect = new Rect();
- Gravity.apply(Gravity.CENTER, mDrawableWidth, mDrawableHeight, selfRect, drawableRect);
- mDrawable.setBounds(drawableRect);
- }
-
- @Override
- @Nullable
- public Drawable getDrawable() {
- return mDrawable;
- }
-
- @Override
- public int getDrawableWidth() {
- return mDrawableWidth;
- }
-
- @Override
- public int getDrawableHeight() {
- return mDrawableHeight;
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- if (mDrawable != null) {
- setDrawableSizeInternal(w, h);
- }
- }
-
- @Override
- protected boolean verifyDrawable(Drawable who) {
- return super.verifyDrawable(who) || who == mDrawable;
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
-
- final Drawable drawable = mDrawable;
- if (drawable != null && drawable.isStateful()
- && drawable.setState(getDrawableState())) {
- invalidateDrawable(drawable);
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- if (mDrawable != null) {
- mDrawable.draw(canvas);
- }
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- return false;
- }
-
- @Override
- public void setContentAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
- }
-
- @Override
- public void setModalAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
- }
-
- /**
- * Set the tint color of the icon, useful for scrimming or dimming.
- *
- * @param color to blend in.
- * @param amount [0,1] 0 no tint, 1 full tint
- */
- @Override
- public void setIconColorTint(int color, float amount) {
- if (mDrawable != null) {
- mDrawable.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
- }
- }
-
- @Override
- public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
- RecentsPagedOrientationHandler orientationHandler =
- orientationState.getOrientationHandler();
- boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
- DeviceProfile deviceProfile =
- ActivityContext.lookupContext(getContext()).getDeviceProfile();
-
- FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) getLayoutParams();
-
- int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
- int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
- int taskMargin = deviceProfile.overviewTaskMarginPx;
-
- orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight,
- thumbnailTopMargin, isRtl);
- iconParams.width = iconParams.height = taskIconHeight;
- setLayoutParams(iconParams);
-
- setRotation(orientationHandler.getDegreesRotated());
- int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
- : deviceProfile.overviewTaskIconDrawableSizePx;
- setDrawableSize(iconDrawableSize, iconDrawableSize);
- }
-
- @Override
- public View asView() {
- return this;
- }
-}
diff --git a/quickstep/src/com/android/quickstep/views/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt
new file mode 100644
index 0000000..583207f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconView.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.view.updateLayoutParams
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.util.RecentsOrientedState
+
+/**
+ * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
+ * when the drawable changes.
+ */
+class IconView : View, TaskViewIcon {
+ private val multiValueAlpha: MultiValueAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS)
+ private var drawable: Drawable? = null
+ private var drawableWidth = 0
+ private var drawableHeight = 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)
+
+ init {
+ multiValueAlpha.setUpdateVisibility(true)
+ }
+
+ /** Sets a [Drawable] to be displayed. */
+ override fun setDrawable(d: Drawable?) {
+ drawable?.callback = null
+
+ drawable = d
+ drawable?.let {
+ it.callback = this
+ setDrawableSizeInternal(width, height)
+ }
+ invalidate()
+ }
+
+ /** Sets the size of the icon drawable. */
+ override fun setDrawableSize(iconWidth: Int, iconHeight: Int) {
+ drawableWidth = iconWidth
+ drawableHeight = iconHeight
+ drawable?.let { setDrawableSizeInternal(width, height) }
+ }
+
+ private fun setDrawableSizeInternal(selfWidth: Int, selfHeight: Int) {
+ val selfRect = Rect(0, 0, selfWidth, selfHeight)
+ val drawableRect = Rect()
+ Gravity.apply(Gravity.CENTER, drawableWidth, drawableHeight, selfRect, drawableRect)
+ drawable?.bounds = drawableRect
+ }
+
+ override fun getDrawable(): Drawable? = drawable
+
+ override fun getDrawableWidth(): Int = drawableWidth
+
+ override fun getDrawableHeight(): Int = drawableHeight
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ drawable?.let { setDrawableSizeInternal(w, h) }
+ }
+
+ override fun verifyDrawable(who: Drawable): Boolean =
+ super.verifyDrawable(who) || who === drawable
+
+ override fun drawableStateChanged() {
+ super.drawableStateChanged()
+ drawable?.let {
+ if (it.isStateful && it.setState(drawableState)) {
+ invalidateDrawable(it)
+ }
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ drawable?.draw(canvas)
+ }
+
+ override fun hasOverlappingRendering(): Boolean = false
+
+ override fun setContentAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_CONTENT_ALPHA].setValue(alpha)
+ }
+
+ override fun setModalAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_MODAL_ALPHA].setValue(alpha)
+ }
+
+ /**
+ * Set the tint color of the icon, useful for scrimming or dimming.
+ *
+ * @param color to blend in.
+ * @param amount [0,1] 0 no tint, 1 full tint
+ */
+ override fun setIconColorTint(color: Int, amount: Float) {
+ drawable?.colorFilter = Utilities.makeColorTintingColorFilter(color, amount)
+ }
+
+ override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) {
+ val orientationHandler = orientationState.orientationHandler
+ val deviceProfile: DeviceProfile =
+ (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile()
+ orientationHandler.setTaskIconParams(
+ iconParams = getLayoutParams() as FrameLayout.LayoutParams,
+ taskIconMargin = deviceProfile.overviewTaskMarginPx,
+ taskIconHeight = deviceProfile.overviewTaskIconSizePx,
+ thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx,
+ isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
+ )
+ updateLayoutParams<FrameLayout.LayoutParams> {
+ height = deviceProfile.overviewTaskIconSizePx
+ width = height
+ }
+ setRotation(orientationHandler.degreesRotated)
+ val iconDrawableSize =
+ if (isGridTask) deviceProfile.overviewTaskIconDrawableSizeGridPx
+ else deviceProfile.overviewTaskIconDrawableSizePx
+ setDrawableSize(iconDrawableSize, iconDrawableSize)
+ }
+
+ override fun asView(): View = this
+
+ companion object {
+ private const val NUM_ALPHA_CHANNELS = 2
+ private const val INDEX_CONTENT_ALPHA = 0
+ private const val INDEX_MODAL_ALPHA = 1
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index e48a7c6..b2d2302 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -54,6 +54,7 @@
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.RotationTouchHelper;
import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AnimUtils;
import com.android.quickstep.util.SplitSelectStateController;
import com.android.systemui.shared.recents.model.Task;
@@ -91,10 +92,12 @@
protected void handleStartHome(boolean animated) {
StateManager stateManager = getStateManager();
animated &= stateManager.shouldAnimateStateChange();
- stateManager.goToState(NORMAL, animated);
- if (FeatureFlags.enableSplitContextually()) {
- mSplitSelectStateController.getSplitAnimationController()
- .playPlaceholderDismissAnim(mContainer, LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
+ if (mSplitSelectStateController.isSplitSelectActive()) {
+ AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+ LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
+ mSplitSelectStateController.getSplitAnimationController());
+ } else {
+ stateManager.goToState(NORMAL, animated);
}
AbstractFloatingView.closeAllOpenViews(mContainer, animated);
}
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index 3d994e8..f6393e4 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -16,13 +16,11 @@
package com.android.quickstep.views;
-import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON;
import static com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
@@ -42,9 +40,8 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.util.AnimUtils;
import com.android.quickstep.util.SplitSelectStateController;
/**
@@ -57,7 +54,6 @@
public class SplitInstructionsView extends LinearLayout {
private static final int BOUNCE_DURATION = 250;
private static final float BOUNCE_HEIGHT = 20;
- private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
private final RecentsViewContainer mContainer;
public boolean mIsCurrentlyAnimating = false;
@@ -165,25 +161,11 @@
private void exitSplitSelection() {
RecentsView recentsView = mContainer.getOverviewPanel();
SplitSelectStateController splitSelectController = recentsView.getSplitSelectController();
-
StateManager stateManager = recentsView.getStateManager();
- BaseState startState = stateManager.getState();
- long duration = startState.getTransitionDuration(mContainer.asContext(), false);
- if (duration == 0) {
- // Case where we're in contextual on workspace (NORMAL), which by default has 0
- // transition duration
- duration = DURATION_DEFAULT_SPLIT_DISMISS;
- }
- StateAnimationConfig config = new StateAnimationConfig();
- config.duration = duration;
- AnimatorSet stateAnim = stateManager.createAtomicAnimation(
- startState, NORMAL, config);
- AnimatorSet dismissAnim = splitSelectController.getSplitAnimationController()
- .createPlaceholderDismissAnim(mContainer,
- LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, duration);
- stateAnim.play(dismissAnim);
- stateManager.setCurrentAnimation(stateAnim, NORMAL);
- stateAnim.start();
+
+ AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+ LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON,
+ splitSelectController.getSplitAnimationController());
}
void ensureProperRotation() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 79725c6..e7a8720 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -33,7 +33,7 @@
import com.android.quickstep.recents.di.get
import com.android.quickstep.recents.di.getScope
import com.android.quickstep.recents.di.inject
-import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.viewmodel.TaskContainerViewModel
import com.android.quickstep.task.thumbnail.TaskThumbnail
import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.task.viewmodel.TaskContainerData
@@ -61,10 +61,18 @@
) {
val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
lateinit var taskContainerData: TaskContainerData
- private val getThumbnailUseCase: GetThumbnailUseCase by RecentsDependencies.inject()
+
private val taskThumbnailViewModel: TaskThumbnailViewModel by
RecentsDependencies.inject(snapshotView)
+ // TODO(b/335649589): Ideally create and obtain this from DI.
+ private val taskContainerViewModel: TaskContainerViewModel by lazy {
+ TaskContainerViewModel(
+ sysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
+ getThumbnailUseCase = RecentsDependencies.get()
+ )
+ }
+
init {
if (enableRefactorTaskThumbnail()) {
require(snapshotView is TaskThumbnailView)
@@ -84,7 +92,7 @@
val splitAnimationThumbnail: Bitmap?
get() =
if (enableRefactorTaskThumbnail()) {
- getThumbnailUseCase.run(task.key.id)
+ taskContainerViewModel.getThumbnail(task.key.id)
} else {
thumbnailViewDeprecated.thumbnail
}
@@ -110,7 +118,9 @@
// TODO(b/350743460) Support sysUiStatusNavFlags for new TTV.
val sysUiStatusNavFlags: Int
get() =
- if (enableRefactorTaskThumbnail()) 0 else thumbnailViewDeprecated.sysUiStatusNavFlags
+ if (enableRefactorTaskThumbnail())
+ taskContainerViewModel.getSysUiStatusNavFlags(task.key.id)
+ else thumbnailViewDeprecated.sysUiStatusNavFlags
/** Builds proto for logging */
val itemInfo: WorkspaceItemInfo
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
new file mode 100644
index 0000000..ba4e206
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 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.recents.usecase
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [SysUiStatusNavFlagsUseCase] */
+class SysUiStatusNavFlagsUseCaseTest {
+ private lateinit var tasksRepository: FakeTasksRepository
+ private lateinit var sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase
+
+ @Before
+ fun setup() {
+ tasksRepository = FakeTasksRepository()
+ sysUiStatusNavFlagsUseCase = SysUiStatusNavFlagsUseCase(tasksRepository)
+ initTaskRepository()
+ }
+
+ @Test
+ fun onLightAppearanceReturnExpectedFlags() {
+ assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(FIRST_TASK_ID))
+ .isEqualTo(FLAGS_APPEARANCE_LIGHT_THEME)
+ }
+
+ @Test
+ fun onDarkAppearanceReturnExpectedFlags() {
+ assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(SECOND_TASK_ID))
+ .isEqualTo(FLAGS_APPEARANCE_DARK_THEME)
+ }
+
+ @Test
+ fun whenThumbnailIsNullReturnDefault() {
+ assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(UNKNOWN_TASK_ID))
+ .isEqualTo(FLAGS_DEFAULT)
+ }
+
+ private fun initTaskRepository() {
+ val firstTask =
+ Task(Task.TaskKey(FIRST_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+ colorBackground = Color.BLACK
+ }
+ val firstThumbnailData =
+ ThumbnailData(
+ thumbnail =
+ mock<Bitmap>().apply {
+ whenever(width).thenReturn(THUMBNAIL_WIDTH)
+ whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+ },
+ appearance = APPEARANCE_LIGHT_THEME
+ )
+
+ val secondTask =
+ Task(Task.TaskKey(SECOND_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2005)).apply {
+ colorBackground = Color.BLACK
+ }
+ val secondThumbnailData =
+ ThumbnailData(
+ thumbnail =
+ mock<Bitmap>().apply {
+ whenever(width).thenReturn(THUMBNAIL_WIDTH)
+ whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+ },
+ appearance = APPEARANCE_DARK_THEME
+ )
+
+ tasksRepository.seedTasks(listOf(firstTask, secondTask))
+ tasksRepository.seedThumbnailData(
+ mapOf(FIRST_TASK_ID to firstThumbnailData, SECOND_TASK_ID to secondThumbnailData)
+ )
+ tasksRepository.setVisibleTasks(listOf(FIRST_TASK_ID, SECOND_TASK_ID))
+ }
+
+ companion object {
+ const val FIRST_TASK_ID = 0
+ const val SECOND_TASK_ID = 100
+ const val UNKNOWN_TASK_ID = 404
+ const val THUMBNAIL_WIDTH = 100
+ const val THUMBNAIL_HEIGHT = 200
+ const val APPEARANCE_LIGHT_THEME = 24
+ const val FLAGS_APPEARANCE_LIGHT_THEME = 5
+ const val APPEARANCE_DARK_THEME = 0
+ const val FLAGS_APPEARANCE_DARK_THEME = 10
+ const val FLAGS_DEFAULT = 0
+ }
+}
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
index ddd3042..ebfa996 100644
--- a/res/drawable/rounded_action_button.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -22,8 +22,5 @@
<stroke
android:width="1dp"
android:color="?attr/materialColorSurfaceContainerLow" />
- <padding
- android:left="@dimen/rounded_button_padding"
- android:right="@dimen/rounded_button_padding" />
</shape>
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index c581ae3..a45d585 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -44,8 +44,7 @@
<FrameLayout
android:layout_width="@dimen/rounded_button_width"
android:layout_height="@dimen/rounded_button_width"
- android:background="@drawable/rounded_action_button"
- android:padding="@dimen/rounded_button_padding">
+ android:background="@drawable/rounded_action_button">
<ImageButton
android:id="@+id/action_btn"
android:layout_width="@dimen/x_icon_size"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 05724e2..af91b5a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -172,8 +172,6 @@
<dimen name="padded_rounded_button_height">48dp</dimen>
<dimen name="rounded_button_height">48dp</dimen>
<dimen name="rounded_button_radius">200dp</dimen>
- <dimen name="rounded_button_padding">8dp</dimen>
-
<!-- Widget tray -->
<dimen name="widget_cell_vertical_padding">8dp</dimen>
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index db693f0..8b1f42b 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -157,7 +157,8 @@
isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
isEventOverAccessibleDropTargetBar(ev);
if (!isOverFolderOrSearchBar) {
- sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+ sendTapOutsideFolderAccessibilityEvent(
+ currentFolder.getIsEditingName());
mHoverPointClosesFolder = true;
return true;
}
@@ -167,7 +168,8 @@
isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
isEventOverAccessibleDropTargetBar(ev);
if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) {
- sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+ sendTapOutsideFolderAccessibilityEvent(
+ currentFolder.getIsEditingName());
mHoverPointClosesFolder = true;
return true;
} else if (!isOverFolderOrSearchBar) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 175ab4e..3edf1f2 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -136,7 +136,8 @@
* We avoid measuring {@link #mContent} with a 0 width or height, as this
* results in CellLayout being measured as UNSPECIFIED, which it does not support.
*/
- private static final int MIN_CONTENT_DIMEN = 5;
+ @VisibleForTesting
+ static final int MIN_CONTENT_DIMEN = 5;
public static final int STATE_CLOSED = 0;
public static final int STATE_ANIMATING = 1;
@@ -144,7 +145,8 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_CLOSED, STATE_ANIMATING, STATE_OPEN})
- public @interface FolderState {}
+ public @interface FolderState {
+ }
/**
* Time for which the scroll hint is shown before automatically changing page.
@@ -165,7 +167,7 @@
private static final int FOLDER_COLOR_ANIMATION_DURATION = 200;
private static final int REORDER_DELAY = 250;
- private static final int ON_EXIT_CLOSE_DELAY = 400;
+ static final int ON_EXIT_CLOSE_DELAY = 400;
private static final Rect sTempRect = new Rect();
private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10;
@@ -185,10 +187,10 @@
|| itemType == ITEM_TYPE_APP_PAIR;
}
- private final Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
- private final Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
- private final Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
- final Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
+ private Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
+ private Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
+ private Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
+ private Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
@@ -198,7 +200,7 @@
// Folder can be displayed in Launcher's activity or a separate window (e.g. Taskbar).
// Anything specific to Launcher should use mLauncherDelegate, otherwise should
// use mActivityContext.
- protected final LauncherDelegate mLauncherDelegate;
+ protected LauncherDelegate mLauncherDelegate;
protected final ActivityContext mActivityContext;
protected DragController mDragController;
@@ -211,7 +213,7 @@
@Thunk
FolderPagedView mContent;
- public FolderNameEditText mFolderName;
+ private FolderNameEditText mFolderName;
private PageIndicatorDots mPageIndicator;
protected View mFooter;
@@ -235,10 +237,10 @@
private OnFolderStateChangedListener mPriorityOnFolderStateChangedListener;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mRearrangeOnClose = false;
- boolean mItemsInvalidated = false;
+ private boolean mItemsInvalidated = false;
private View mCurrentDragView;
private boolean mIsExternalDrag;
- private boolean mDragInProgress = false;
+ private boolean mIsDragInProgress = false;
private boolean mDeleteFolderOnDropCompleted = false;
private boolean mSuppressFolderDeletion = false;
private boolean mItemAddedBackToSelfViaIcon = false;
@@ -251,7 +253,7 @@
private int mScrollAreaOffset;
@Thunk
- int mScrollHintDir = SCROLL_NONE;
+ private int mScrollHintDir = SCROLL_NONE;
@Thunk
int mCurrentScrollDir = SCROLL_NONE;
@@ -316,9 +318,9 @@
| InputType.TYPE_TEXT_FLAG_CAP_WORDS);
mFolderName.forceDisableSuggestions(true);
mFolderName.setPadding(mFolderName.getPaddingLeft(),
- (mFooterHeight - mFolderName.getLineHeight()) / 2,
+ (getFooterHeight() - mFolderName.getLineHeight()) / 2,
mFolderName.getPaddingRight(),
- (mFooterHeight - mFolderName.getLineHeight()) / 2);
+ (getFooterHeight() - mFolderName.getLineHeight()) / 2);
mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
@@ -326,42 +328,54 @@
public boolean onLongClick(View v) {
// Return if global dragging is not enabled
- if (!mLauncherDelegate.isDraggingEnabled()) return true;
+ if (!getIsLauncherDraggingEnabled()) return true;
return startDrag(v, new DragOptions());
}
+ @VisibleForTesting
+ boolean getIsLauncherDraggingEnabled() {
+ return mLauncherDelegate.isDraggingEnabled();
+ }
+
public boolean startDrag(View v, DragOptions options) {
Object tag = v.getTag();
if (tag instanceof ItemInfo item) {
mEmptyCellRank = item.rank;
mCurrentDragView = v;
- mDragController.addDragListener(this);
- if (options.isAccessibleDrag) {
- mDragController.addDragListener(new AccessibleDragListenerAdapter(
- mContent, FolderAccessibilityHelper::new) {
- @Override
- protected void enableAccessibleDrag(boolean enable,
- @Nullable DragObject dragObject) {
- super.enableAccessibleDrag(enable, dragObject);
- mFooter.setImportantForAccessibility(enable
- ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
- : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
- }
- });
- }
-
- mLauncherDelegate.beginDragShared(v, this, options);
+ addDragListener(options);
+ callBeginDragShared(v, options);
}
return true;
}
+ void callBeginDragShared(View v, DragOptions options) {
+ mLauncherDelegate.beginDragShared(v, this, options);
+ }
+
+ void addDragListener(DragOptions options) {
+ getDragController().addDragListener(this);
+ if (!options.isAccessibleDrag) {
+ return;
+ }
+ getDragController().addDragListener(new AccessibleDragListenerAdapter(
+ mContent, FolderAccessibilityHelper::new) {
+ @Override
+ protected void enableAccessibleDrag(boolean enable,
+ @Nullable DragObject dragObject) {
+ super.enableAccessibleDrag(enable, dragObject);
+ mFooter.setImportantForAccessibility(enable
+ ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
+ });
+ }
+
@Override
public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
if (dragObject.dragSource != this) {
return;
}
-
mContent.removeItem(mCurrentDragView);
mItemsInvalidated = true;
@@ -370,29 +384,23 @@
try (SuppressInfoChanges s = new SuppressInfoChanges()) {
mInfo.remove(dragObject.dragInfo, true);
}
- mDragInProgress = true;
+ mIsDragInProgress = true;
mItemAddedBackToSelfViaIcon = false;
}
@Override
public void onDragEnd() {
- if (mIsExternalDrag && mDragInProgress) {
+ if (mIsExternalDrag && mIsDragInProgress) {
completeDragExit();
}
- mDragInProgress = false;
- mDragController.removeDragListener(this);
- }
-
- public boolean isEditingName() {
- return mIsEditingName;
+ mIsDragInProgress = false;
+ getDragController().removeDragListener(this);
}
public void startEditingFolderName() {
- post(() -> {
- showLabelSuggestions();
- mFolderName.setHint("");
- mIsEditingName = true;
- });
+ showLabelSuggestions();
+ mFolderName.setHint("");
+ mIsEditingName = true;
}
@Override
@@ -460,7 +468,11 @@
return mFolderIcon;
}
- public void setDragController(DragController dragController) {
+ DragController getDragController() {
+ return mDragController;
+ }
+
+ void setDragController(DragController dragController) {
mDragController = dragController;
}
@@ -541,7 +553,7 @@
* Show suggested folder title in FolderEditText if the first suggestion is non-empty, push
* rest of the suggestions to InputMethodManager.
*/
- private void showLabelSuggestions() {
+ void showLabelSuggestions() {
if (mInfo.suggestedFolderNames == null) {
return;
}
@@ -635,11 +647,11 @@
*/
public void beginExternalDrag() {
mIsExternalDrag = true;
- mDragInProgress = true;
+ mIsDragInProgress = true;
// Since this folder opened by another controller, it might not get onDrop or
// onDropComplete. Perform cleanup once drag-n-drop ends.
- mDragController.addDragListener(this);
+ getDragController().addDragListener(this);
ArrayList<ItemInfo> items = new ArrayList<>(mInfo.getContents());
mEmptyCellRank = items.size();
@@ -663,16 +675,12 @@
* is played.
*/
private void animateOpen(List<ItemInfo> items, int pageNo) {
- if (items == null || items.size() <= 1) {
- Log.d(TAG, "Couldn't animate folder open because items is: " + items);
+ if (!shouldAnimateOpen(items)) {
return;
}
Folder openFolder = getOpen(mActivityContext);
- if (openFolder != null && openFolder != this) {
- // Close any open folder before opening a folder.
- openFolder.close(true);
- }
+ closeOpenFolder(openFolder);
mContent.bindItems(items);
centerAboutIcon();
@@ -686,7 +694,7 @@
// There was a one-off crash where the folder had a parent already.
if (getParent() == null) {
dragLayer.addView(this);
- mDragController.addDropTarget(this);
+ getDragController().addDropTarget(this);
} else {
if (FeatureFlags.IS_STUDIO_BUILD) {
Log.e(TAG, "Opening folder (" + this + ") which already has a parent:"
@@ -735,7 +743,7 @@
// Do not update the flag if we are in drag mode. The flag will be updated, when we
// actually drop the icon.
- final boolean updateAnimationFlag = !mDragInProgress;
+ final boolean updateAnimationFlag = !mIsDragInProgress;
anim.addListener(new AnimatorListenerAdapter() {
@SuppressLint("InlinedApi")
@@ -769,12 +777,36 @@
anim.start();
// Make sure the folder picks up the last drag move even if the finger doesn't move.
- if (mDragController.isDragging()) {
- mDragController.forceTouchMove();
+ if (getDragController().isDragging()) {
+ getDragController().forceTouchMove();
}
mContent.verifyVisibleHighResIcons(mContent.getNextPage());
}
+ /**
+ * Determines whether we should animate the folder opening.
+ */
+ boolean shouldAnimateOpen(List<ItemInfo> items) {
+ if (items == null || items.size() <= 1) {
+ Log.d(TAG, "Couldn't animate folder open because items is: " + items);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * If there's a folder already open, we want to close it before opening another one.
+ */
+ @VisibleForTesting
+ boolean closeOpenFolder(Folder openFolder) {
+ if (openFolder != null && openFolder != this) {
+ // Close any open folder before opening a folder.
+ openFolder.close(true);
+ return true;
+ }
+ return false;
+ }
+
@Override
protected boolean isOfType(int type) {
return (type & TYPE_FOLDER) != 0;
@@ -788,7 +820,7 @@
mCurrentAnimator.cancel();
}
- if (isEditingName()) {
+ if (mIsEditingName) {
mFolderName.dispatchBackKey();
}
@@ -872,7 +904,7 @@
if (parent != null) {
parent.removeView(this);
}
- mDragController.removeDropTarget(this);
+ getDragController().removeDropTarget(this);
clearFocus();
if (mFolderIcon != null) {
mFolderIcon.setVisibility(View.VISIBLE);
@@ -893,12 +925,12 @@
mRearrangeOnClose = false;
}
if (getItemCount() <= 1) {
- if (!mDragInProgress && !mSuppressFolderDeletion) {
+ if (!mIsDragInProgress && !mSuppressFolderDeletion) {
replaceFolderWithFinalItem();
- } else if (mDragInProgress) {
+ } else if (mIsDragInProgress) {
mDeleteFolderOnDropCompleted = true;
}
- } else if (!mDragInProgress) {
+ } else if (!mIsDragInProgress) {
mContent.unbindItems();
}
mSuppressFolderDeletion = false;
@@ -1018,7 +1050,8 @@
}
}
- private void clearDragInfo() {
+ @VisibleForTesting
+ void clearDragInfo() {
mCurrentDragView = null;
mIsExternalDrag = false;
}
@@ -1059,7 +1092,8 @@
if (getItemCount() <= 1) {
mDeleteFolderOnDropCompleted = true;
}
- if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) {
+ if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon
+ && target != this) {
replaceFolderWithFinalItem();
}
} else {
@@ -1090,7 +1124,7 @@
}
mDeleteFolderOnDropCompleted = false;
- mDragInProgress = false;
+ mIsDragInProgress = false;
mItemAddedBackToSelfViaIcon = false;
mCurrentDragView = null;
@@ -1133,7 +1167,7 @@
}
public void notifyDrop() {
- if (mDragInProgress) {
+ if (mIsDragInProgress) {
mItemAddedBackToSelfViaIcon = true;
}
}
@@ -1176,28 +1210,41 @@
}
protected int getContentAreaHeight() {
- DeviceProfile grid = mActivityContext.getDeviceProfile();
- int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y
- - mFooterHeight;
- int height = Math.min(maxContentAreaHeight,
+ int height = Math.min(getMaxContentAreaHeight(),
mContent.getDesiredHeight());
return Math.max(height, MIN_CONTENT_DIMEN);
}
- private int getContentAreaWidth() {
+ @VisibleForTesting
+ int getMaxContentAreaHeight() {
+ DeviceProfile grid = mActivityContext.getDeviceProfile();
+ return grid.availableHeightPx - grid.getTotalWorkspacePadding().y
+ - getFooterHeight();
+ }
+
+ @VisibleForTesting
+ int getContentAreaWidth() {
return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN);
}
- private int getFolderWidth() {
+ @VisibleForTesting
+ int getFolderWidth() {
return getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
}
- private int getFolderHeight() {
+ @VisibleForTesting
+ int getFolderHeight() {
return getFolderHeight(getContentAreaHeight());
}
- private int getFolderHeight(int contentAreaHeight) {
- return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight;
+ @VisibleForTesting
+ int getFolderHeight(int contentAreaHeight) {
+ return getPaddingTop() + getPaddingBottom() + contentAreaHeight + getFooterHeight();
+ }
+
+ @VisibleForTesting
+ int getFooterHeight() {
+ return mFooterHeight;
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@@ -1367,7 +1414,7 @@
}
// Clear the drag info, as it is no longer being dragged.
- mDragInProgress = false;
+ mIsDragInProgress = false;
if (mContent.getPageCount() > 1) {
// The animation has already been shown while opening the folder.
@@ -1436,7 +1483,8 @@
}
}
- private View getViewForInfo(final ItemInfo item) {
+ @VisibleForTesting
+ View getViewForInfo(final ItemInfo item) {
return mContent.iterateOverItems((info, view) -> info == item);
}
@@ -1494,7 +1542,7 @@
if (hasFocus) {
mFromLabelState = mInfo.getFromLabelState();
mFromTitle = mInfo.title;
- startEditingFolderName();
+ post(this::startEditingFolderName);
} else {
StatsLogger statsLogger = mStatsLogManager.logger()
.withItemInfo(mInfo)
@@ -1627,7 +1675,7 @@
/** Navigation bar back key or hardware input back key has been issued. */
@Override
public void onBackInvoked() {
- if (isEditingName()) {
+ if (mIsEditingName) {
mFolderName.dispatchBackKey();
} else {
super.onBackInvoked();
@@ -1639,7 +1687,7 @@
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
BaseDragLayer dl = (BaseDragLayer) getParent();
- if (isEditingName()) {
+ if (mIsEditingName) {
if (!dl.isEventOverView(mFolderName, ev)) {
mFolderName.dispatchBackKey();
return true;
@@ -1686,6 +1734,95 @@
return mContent;
}
+ @VisibleForTesting
+ void setItemAddedBackToSelfViaIcon(boolean value) {
+ mItemAddedBackToSelfViaIcon = value;
+ }
+
+ @VisibleForTesting
+ boolean getItemAddedBackToSelfViaIcon() {
+ return mItemAddedBackToSelfViaIcon;
+ }
+
+ @VisibleForTesting
+ void setIsDragInProgress(boolean value) {
+ mIsDragInProgress = value;
+ }
+
+ @VisibleForTesting
+ boolean getIsDragInProgress() {
+ return mIsDragInProgress;
+ }
+
+ @VisibleForTesting
+ View getCurrentDragView() {
+ return mCurrentDragView;
+ }
+
+ @VisibleForTesting
+ void setCurrentDragView(View view) {
+ mCurrentDragView = view;
+ }
+
+ @VisibleForTesting
+ boolean getItemsInvalidated() {
+ return mItemsInvalidated;
+ }
+
+ @VisibleForTesting
+ void setItemsInvalidated(boolean value) {
+ mItemsInvalidated = value;
+ }
+
+ @VisibleForTesting
+ boolean getIsExternalDrag() {
+ return mIsExternalDrag;
+ }
+
+ @VisibleForTesting
+ void setIsExternalDrag(boolean value) {
+ mIsExternalDrag = value;
+ }
+
+ public boolean getIsEditingName() {
+ return mIsEditingName;
+ }
+
+ @VisibleForTesting
+ void setIsEditingName(boolean value) {
+ mIsEditingName = value;
+ }
+
+ @VisibleForTesting
+ void setFolderName(FolderNameEditText value) {
+ mFolderName = value;
+ }
+
+ @VisibleForTesting
+ FolderNameEditText getFolderName() {
+ return mFolderName;
+ }
+
+ @VisibleForTesting
+ boolean getIsOpen() {
+ return mIsOpen;
+ }
+
+ @VisibleForTesting
+ void setIsOpen(boolean value) {
+ mIsOpen = value;
+ }
+
+ @VisibleForTesting
+ boolean getRearrangeOnClose() {
+ return mRearrangeOnClose;
+ }
+
+ @VisibleForTesting
+ void setRearrangeOnClose(boolean value) {
+ mRearrangeOnClose = value;
+ }
+
/** Returns the height of the current folder's bottom edge from the bottom of the screen. */
private int getHeightFromBottom() {
BaseDragLayer.LayoutParams layoutParams = (BaseDragLayer.LayoutParams) getLayoutParams();
@@ -1696,10 +1833,15 @@
}
@VisibleForTesting
- public boolean getDeleteFolderOnDropCompleted() {
+ boolean getDeleteFolderOnDropCompleted() {
return mDeleteFolderOnDropCompleted;
}
+ @VisibleForTesting
+ void setDeleteFolderOnDropCompleted(boolean value) {
+ mDeleteFolderOnDropCompleted = value;
+ }
+
/**
* Save this listener for the special case of when we update the state and concurrently
* add another listener to {@link #mOnFolderStateChangedListeners} to avoid a
@@ -1709,7 +1851,13 @@
mPriorityOnFolderStateChangedListener = listener;
}
- private void setState(@FolderState int newState) {
+ @VisibleForTesting
+ int getState() {
+ return mState;
+ }
+
+ @VisibleForTesting
+ void setState(@FolderState int newState) {
mState = newState;
if (mPriorityOnFolderStateChangedListener != null) {
mPriorityOnFolderStateChangedListener.onFolderStateChanged(mState);
@@ -1721,6 +1869,60 @@
}
}
+ @VisibleForTesting
+ Alarm getOnExitAlarm() {
+ return mOnExitAlarm;
+ }
+
+ @VisibleForTesting
+ void setOnExitAlarm(Alarm value) {
+ mOnExitAlarm = value;
+ }
+
+ @VisibleForTesting
+ Alarm getReorderAlarm() {
+ return mReorderAlarm;
+ }
+
+ @VisibleForTesting
+ void setReorderAlarm(Alarm value) {
+ mReorderAlarm = value;
+ }
+
+ @VisibleForTesting
+ Alarm getOnScrollHintAlarm() {
+ return mOnScrollHintAlarm;
+ }
+
+ @VisibleForTesting
+ void setOnScrollHintAlarm(Alarm value) {
+ mOnScrollHintAlarm = value;
+ }
+
+ @VisibleForTesting
+ Alarm getScrollPauseAlarm() {
+ return mScrollPauseAlarm;
+ }
+
+ @VisibleForTesting
+ void setScrollPauseAlarm(Alarm value) {
+ mScrollPauseAlarm = value;
+ }
+
+ @VisibleForTesting
+ int getScrollHintDir() {
+ return mScrollHintDir;
+ }
+
+ @VisibleForTesting
+ void setScrollHintDir(int value) {
+ mScrollHintDir = value;
+ }
+
+ @VisibleForTesting
+ int getScrollAreaOffset() {
+ return mScrollAreaOffset;
+ }
/**
* Adds the provided listener to the running list of Folder listeners
* {@link #mOnFolderStateChangedListeners}
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 3c4cf5a..588a6db 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -256,8 +256,8 @@
mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening));
// Fade in the folder name, as the text can overlap the icons when grid size is small.
- mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f);
- play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1),
+ mFolder.getFolderName().setAlpha(mIsOpening ? 0f : 1f);
+ play(a, getAnimator(mFolder.getFolderName(), View.ALPHA, 0, 1),
mIsOpening ? FOLDER_NAME_ALPHA_DURATION : 0,
mIsOpening ? mDuration - FOLDER_NAME_ALPHA_DURATION : FOLDER_NAME_ALPHA_DURATION);
@@ -318,7 +318,7 @@
mFolder.mFooter.setScaleX(1f);
mFolder.mFooter.setScaleY(1f);
mFolder.mFooter.setTranslationX(0f);
- mFolder.mFolderName.setAlpha(1f);
+ mFolder.getFolderName().setAlpha(1f);
mFolder.setClipChildren(mFolderClipChildren);
mFolder.setClipToPadding(mFolderClipToPadding);
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index e9859cf..a0b695a 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -459,7 +459,7 @@
mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter());
onTitleChanged(mInfo.title);
- mFolder.mFolderName.setText(mInfo.title);
+ mFolder.getFolderName().setText(mInfo.title);
// Logging for folder creation flow
StatsLogManager.newInstance(getContext()).logger()
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 1b5ef42..9dc2d24 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -373,8 +373,8 @@
// Update footer
mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
// Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text.
- mFolder.mFolderName.setGravity(getPageCount() > 1 ?
- (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
+ mFolder.getFolderName().setGravity(getPageCount() > 1
+ ? (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
}
public int getDesiredWidth() {
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index e44ea1d..a691e45 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -43,6 +43,7 @@
import android.view.animation.OvershootInterpolator;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
@@ -131,7 +132,8 @@
private float mCurrentPosition;
private float mFinalPosition;
private boolean mIsScrollPaused;
- private boolean mIsTwoPanels;
+ @VisibleForTesting
+ boolean mIsTwoPanels;
private ObjectAnimator mAnimator;
private @Nullable ObjectAnimator mAlphaAnimator;
@@ -477,6 +479,21 @@
return sTempRect;
}
+ @VisibleForTesting
+ int getActivePage() {
+ return mActivePage;
+ }
+
+ @VisibleForTesting
+ int getNumPages() {
+ return mNumPages;
+ }
+
+ @VisibleForTesting
+ float getCurrentPosition() {
+ return mCurrentPosition;
+ }
+
private class MyOutlineProver extends ViewOutlineProvider {
@Override
diff --git a/src/com/android/launcher3/util/ShortcutUtil.java b/src/com/android/launcher3/util/ShortcutUtil.java
index 07b7941..aa4f8af 100644
--- a/src/com/android/launcher3/util/ShortcutUtil.java
+++ b/src/com/android/launcher3/util/ShortcutUtil.java
@@ -54,14 +54,6 @@
? ((WorkspaceItemInfo) info).getPersonKeys() : Utilities.EMPTY_STRING_ARRAY;
}
- /**
- * Returns true if the item is a deep shortcut.
- */
- public static boolean isDeepShortcut(ItemInfo info) {
- return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
- && info instanceof WorkspaceItemInfo;
- }
-
private static boolean isActive(ItemInfo info) {
boolean isLoading = info instanceof WorkspaceItemInfo
&& ((WorkspaceItemInfo) info).hasPromiseIconUi();
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 51749a7..6bae1ba 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -31,6 +31,7 @@
import android.provider.Settings;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.Utilities;
@@ -49,14 +50,14 @@
public static final VibrationEffect EFFECT_CLICK =
createPredefined(VibrationEffect.EFFECT_CLICK);
- private static final Uri HAPTIC_FEEDBACK_URI =
- Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED);
+ @VisibleForTesting
+ static final Uri HAPTIC_FEEDBACK_URI = Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED);
- private static final float LOW_TICK_SCALE = 0.9f;
- private static final float DRAG_TEXTURE_SCALE = 0.03f;
- private static final float DRAG_COMMIT_SCALE = 0.5f;
- private static final float DRAG_BUMP_SCALE = 0.4f;
- private static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
+ @VisibleForTesting static final float LOW_TICK_SCALE = 0.9f;
+ @VisibleForTesting static final float DRAG_TEXTURE_SCALE = 0.03f;
+ @VisibleForTesting static final float DRAG_COMMIT_SCALE = 0.5f;
+ @VisibleForTesting static final float DRAG_BUMP_SCALE = 0.4f;
+ @VisibleForTesting static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
@Nullable
private final VibrationEffect mDragEffect;
@@ -73,22 +74,29 @@
*/
public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK;
- private final Context mContext;
private final Vibrator mVibrator;
private final boolean mHasVibrator;
- private final SettingsCache.OnChangeListener mHapticChangeListener =
+
+ private final SettingsCache mSettingsCache;
+
+ @VisibleForTesting
+ final SettingsCache.OnChangeListener mHapticChangeListener =
isEnabled -> mIsHapticFeedbackEnabled = isEnabled;
private boolean mIsHapticFeedbackEnabled;
private VibratorWrapper(Context context) {
- mContext = context;
- mVibrator = context.getSystemService(Vibrator.class);
+ this(context.getSystemService(Vibrator.class), SettingsCache.INSTANCE.get(context));
+ }
+
+ @VisibleForTesting
+ VibratorWrapper(Vibrator vibrator, SettingsCache settingsCache) {
+ mVibrator = vibrator;
mHasVibrator = mVibrator.hasVibrator();
+ mSettingsCache = settingsCache;
if (mHasVibrator) {
- SettingsCache cache = SettingsCache.INSTANCE.get(mContext);
- cache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
- mIsHapticFeedbackEnabled = cache.getValue(HAPTIC_FEEDBACK_URI, 0);
+ mSettingsCache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
+ mIsHapticFeedbackEnabled = mSettingsCache.getValue(HAPTIC_FEEDBACK_URI, 0);
} else {
mIsHapticFeedbackEnabled = false;
}
@@ -98,12 +106,7 @@
// Drag texture, Commit, and Bump should only be used for premium phones.
// Before using these haptics make sure check if the device can use it
- VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
- for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
- dragEffect.addPrimitive(
- PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
- }
- mDragEffect = dragEffect.compose();
+ mDragEffect = getDragEffect();
mCommitEffect = VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose();
mBumpEffect = VibrationEffect.startComposition().addPrimitive(
@@ -124,8 +127,7 @@
@Override
public void close() {
if (mHasVibrator) {
- SettingsCache.INSTANCE.get(mContext)
- .unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
+ mSettingsCache.unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
}
}
@@ -215,4 +217,13 @@
vibrate(primitiveLowTickEffect);
}
}
+
+ static VibrationEffect getDragEffect() {
+ VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
+ for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
+ dragEffect.addPrimitive(
+ PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
+ }
+ return dragEffect.compose();
+ }
}
diff --git a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
index 104209e..856f4b3 100644
--- a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
@@ -110,7 +110,7 @@
}
View background = RoundedCornerEnforcement.findBackground(this);
if (background == null
- || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
+ || RoundedCornerEnforcement.hasAppWidgetOptedOut(background)) {
resetRoundedCorners();
return;
}
diff --git a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
index 1e46ffd..2e5e251 100644
--- a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
+++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
@@ -67,7 +67,7 @@
/**
* Check whether the app widget has opted out of the enforcement.
*/
- public static boolean hasAppWidgetOptedOut(@NonNull View appWidget, @NonNull View background) {
+ public static boolean hasAppWidgetOptedOut(@NonNull View background) {
return background.getId() == android.R.id.background && background.getClipToOutline();
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
new file mode 100644
index 0000000..3dd8dbc
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2024 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.pm
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.ApplicationInfo.FLAG_INSTALLED
+import android.content.pm.LauncherApps
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.PROMISE_ICON_IDS
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InstallSessionHelperTest {
+
+ private val launcherModelHelper = LauncherModelHelper()
+ private val sandboxContext = spy(launcherModelHelper.sandboxContext)
+ private val packageManager = sandboxContext.packageManager
+ private val expectedAppPackage = "expectedAppPackage"
+ private val expectedInstallerPackage = "expectedInstallerPackage"
+ private val mockPackageInstaller: PackageInstaller = mock()
+
+ private lateinit var installSessionHelper: InstallSessionHelper
+ private lateinit var launcherApps: LauncherApps
+
+ @Before
+ fun setup() {
+ whenever(packageManager.packageInstaller).thenReturn(mockPackageInstaller)
+ whenever(sandboxContext.packageName).thenReturn(expectedInstallerPackage)
+ launcherApps = sandboxContext.spyService(LauncherApps::class.java)
+ installSessionHelper = InstallSessionHelper(sandboxContext)
+ }
+
+ @Test
+ fun `getActiveSessions fetches verified install sessions from LauncherApps`() {
+ // Given
+ val expectedVerifiedSession1 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 0
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ val expectedVerifiedSession2 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+ // When
+ val actualSessions = installSessionHelper.getActiveSessions()
+ // Then
+ assertThat(actualSessions.values.toList()).isEqualTo(expectedSessions)
+ }
+
+ @Test
+ fun `getActiveSessionInfo fetches verified install sessions for given user and pkg`() {
+ // Given
+ val expectedVerifiedSession =
+ PackageInstaller.SessionInfo().apply {
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ whenever(launcherApps.allPackageInstallerSessions)
+ .thenReturn(listOf(expectedVerifiedSession))
+ // When
+ val actualSession =
+ installSessionHelper.getActiveSessionInfo(UserHandle(0), expectedAppPackage)
+ // Then
+ assertThat(actualSession).isEqualTo(expectedVerifiedSession)
+ }
+
+ @Test
+ fun `getVerifiedSessionInfo verifies and returns session for given id`() {
+ // Given
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ whenever(mockPackageInstaller.getSessionInfo(1)).thenReturn(expectedSession)
+ // When
+ val actualSession = installSessionHelper.getVerifiedSessionInfo(1)
+ // Then
+ assertThat(actualSession).isEqualTo(expectedSession)
+ }
+
+ @Test
+ fun `isTrustedPackage returns true if LauncherApps finds ApplicationInfo`() {
+ // Given
+ val expectedApplicationInfo =
+ ApplicationInfo().apply {
+ flags = flags or FLAG_INSTALLED
+ enabled = true
+ }
+ doReturn(expectedApplicationInfo)
+ .whenever(launcherApps)
+ .getApplicationInfo(expectedAppPackage, ApplicationInfo.FLAG_SYSTEM, UserHandle(0))
+ // When
+ val actualResult = installSessionHelper.isTrustedPackage(expectedAppPackage, UserHandle(0))
+ // Then
+ assertThat(actualResult).isTrue()
+ }
+
+ @Test
+ fun `getAllVerifiedSessions verifies and returns all active install sessions`() {
+ // Given
+ val expectedVerifiedSession1 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 0
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ userId = 0
+ }
+ val expectedVerifiedSession2 =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+ // When
+ val actualSessions = installSessionHelper.allVerifiedSessions
+ // Then
+ assertThat(actualSessions).isEqualTo(expectedSessions)
+ }
+
+ @Test
+ fun `promiseIconAddedForId returns true if there is a promiseIcon with the session id`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var actualResult = false
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ actualResult = installSessionHelper.promiseIconAddedForId(1)
+ }
+ // Then
+ assertThat(actualResult).isTrue()
+ }
+
+ @Test
+ fun `removePromiseIconId removes promiseIconId for given Session id`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "app2"
+ userId = 0
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var actualResult = true
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ installSessionHelper.removePromiseIconId(1)
+ actualResult = installSessionHelper.promiseIconAddedForId(1)
+ }
+ // Then
+ assertThat(actualResult).isFalse()
+ }
+
+ @Test
+ fun `tryQueuePromiseAppIcon will update promise icon ids from eligible sessions`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "appPackage"
+ userId = 0
+ appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+ appLabel = "appLabel"
+ installReason = PackageManager.INSTALL_REASON_USER
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var wasPromiseIconAdded = false
+ var actualPromiseIconIds = ""
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ installSessionHelper.removePromiseIconId(1)
+ installSessionHelper.tryQueuePromiseAppIcon(expectedSession)
+ wasPromiseIconAdded = installSessionHelper.promiseIconAddedForId(1)
+ actualPromiseIconIds = LauncherPrefs.get(sandboxContext).get(PROMISE_ICON_IDS)
+ }
+ // Then
+ assertThat(wasPromiseIconAdded).isTrue()
+ assertThat(actualPromiseIconIds).isEqualTo(expectedIdString)
+ }
+
+ @Test
+ fun `verifySessionInfo is true if can verify given SessionInfo`() {
+ // Given
+ val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+ LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+ val expectedSession =
+ PackageInstaller.SessionInfo().apply {
+ sessionId = 1
+ installerPackageName = expectedInstallerPackage
+ appPackageName = "appPackage"
+ userId = 0
+ appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+ appLabel = "appLabel"
+ installReason = PackageManager.INSTALL_REASON_USER
+ }
+ whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+ // When
+ var actualResult = false
+ TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+ actualResult = installSessionHelper.verifySessionInfo(expectedSession)
+ }
+ // Then
+ assertThat(actualResult).isTrue()
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt
new file mode 100644
index 0000000..c43e563
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 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.util
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Utilities
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutUtilTest {
+
+ @Test
+ fun `supportsShortcuts returns true if the item is active and it is an app`() {
+ // A blank workspace item info should be active and should be ITEM_TYPE_APPLICATION
+ val itemInfo = WorkspaceItemInfo()
+ // Action
+ val result = ShortcutUtil.supportsShortcuts(itemInfo)
+ // Verify
+ assertEquals(true, result)
+ }
+
+ @Test
+ fun `supportsDeepShortcuts returns true if the app is active and an app and widgets are enabled`() {
+ // Setup
+ val itemInfo = WorkspaceItemInfo()
+ // Action
+ val result = ShortcutUtil.supportsDeepShortcuts(itemInfo)
+ // Verify
+ assertEquals(true, result)
+ }
+
+ @Test
+ fun `getShortcutIdIfPinnedShortcut returns null if the item is an app`() {
+ // Setup
+ val itemInfo = WorkspaceItemInfo()
+ // Action
+ val result = ShortcutUtil.getShortcutIdIfPinnedShortcut(itemInfo)
+ // Verify
+ assertNull(result)
+ }
+
+ @Test
+ fun `getPersonKeysIfPinnedShortcut returns empty string array if item type is an app`() {
+ // Setup
+ val itemInfo = WorkspaceItemInfo()
+ // Action
+ val result = ShortcutUtil.getPersonKeysIfPinnedShortcut(itemInfo)
+ // Verify
+ assertArrayEquals(Utilities.EMPTY_STRING_ARRAY, result)
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
new file mode 100644
index 0000000..330c394
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2024 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.util
+
+import android.media.AudioAttributes
+import android.os.SystemClock
+import android.os.VibrationEffect
+import android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK
+import android.os.VibrationEffect.Composition.PRIMITIVE_TICK
+import android.os.Vibrator
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.VibratorWrapper.HAPTIC_FEEDBACK_URI
+import com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC
+import com.android.launcher3.util.VibratorWrapper.VIBRATION_ATTRS
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.never
+import org.mockito.kotlin.same
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class VibratorWrapperTest {
+
+ @Mock private lateinit var settingsCache: SettingsCache
+ @Mock private lateinit var vibrator: Vibrator
+ @Captor private lateinit var vibrationEffectCaptor: ArgumentCaptor<VibrationEffect>
+
+ private lateinit var underTest: VibratorWrapper
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ `when`(settingsCache.getValue(HAPTIC_FEEDBACK_URI, 0)).thenReturn(true)
+ `when`(vibrator.hasVibrator()).thenReturn(true)
+ `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_TICK)).thenReturn(true)
+ `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)).thenReturn(true)
+ `when`(vibrator.getPrimitiveDurations(PRIMITIVE_LOW_TICK)).thenReturn(intArrayOf(10))
+
+ underTest = VibratorWrapper(vibrator, settingsCache)
+ }
+
+ @Test
+ fun init_register_onChangeListener() {
+ verify(settingsCache).register(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener)
+ }
+
+ @Test
+ fun close_unregister_onChangeListener() {
+ underTest.close()
+
+ verify(settingsCache).unregister(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener)
+ }
+
+ @Test
+ fun vibrate() {
+ underTest.vibrate(OVERVIEW_HAPTIC)
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS)
+ }
+
+ @Test
+ fun vibrate_primitive_id() {
+ underTest.vibrate(PRIMITIVE_TICK, 1f, OVERVIEW_HAPTIC)
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+ val expectedEffect =
+ VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 1f).compose()
+ assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+ }
+
+ @Test
+ fun vibrate_with_invalid_primitive_id_use_fallback_effect() {
+ underTest.vibrate(-1, 1f, OVERVIEW_HAPTIC)
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS)
+ }
+
+ @Test
+ fun vibrate_for_taskbar_unstash() {
+ underTest.vibrateForTaskbarUnstash()
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+ val expectedEffect =
+ VibrationEffect.startComposition()
+ .addPrimitive(PRIMITIVE_LOW_TICK, VibratorWrapper.LOW_TICK_SCALE)
+ .compose()
+ assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+ }
+
+ @Test
+ fun vibrate_for_drag_bump() {
+ underTest.vibrateForDragBump()
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+ val expectedEffect =
+ VibrationEffect.startComposition()
+ .addPrimitive(PRIMITIVE_LOW_TICK, VibratorWrapper.DRAG_BUMP_SCALE)
+ .compose()
+ assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+ }
+
+ @Test
+ fun vibrate_for_drag_commit() {
+ underTest.vibrateForDragCommit()
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+ val expectedEffect =
+ VibrationEffect.startComposition()
+ .addPrimitive(PRIMITIVE_TICK, VibratorWrapper.DRAG_COMMIT_SCALE)
+ .compose()
+ assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+ }
+
+ @Test
+ fun vibrate_for_drag_texture() {
+ SystemClock.setCurrentTimeMillis(40000)
+
+ underTest.vibrateForDragTexture()
+
+ awaitTasksCompleted()
+ verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+ assertThat(vibrationEffectCaptor.value).isEqualTo(VibratorWrapper.getDragEffect())
+ }
+
+ @Test
+ fun vibrate_for_drag_texture_within_time_window_noOp() {
+ SystemClock.setCurrentTimeMillis(40000)
+ underTest.vibrateForDragTexture()
+ awaitTasksCompleted()
+ reset(vibrator)
+
+ underTest.vibrateForDragTexture()
+
+ verifyNoMoreInteractions(vibrator)
+ }
+
+ @Test
+ fun haptic_feedback_disabled_no_vibrate() {
+ `when`(vibrator.hasVibrator()).thenReturn(false)
+ underTest = VibratorWrapper(vibrator, settingsCache)
+
+ underTest.vibrate(OVERVIEW_HAPTIC)
+
+ awaitTasksCompleted()
+ verify(vibrator, never())
+ .vibrate(any(VibrationEffect::class.java), any(AudioAttributes::class.java))
+ }
+
+ @Test
+ fun cancel_vibrate() {
+ underTest.cancelVibrate()
+
+ awaitTasksCompleted()
+ verify(vibrator).cancel()
+ }
+
+ private fun awaitTasksCompleted() {
+ Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
new file mode 100644
index 0000000..db77702
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 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.widget
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.R
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RoundedCornerEnforcementTest {
+
+ @Test
+ fun `Widget view has one background`() {
+ val mockWidgetView = mock(LauncherAppWidgetHostView::class.java)
+
+ doReturn(android.R.id.background).whenever(mockWidgetView).id
+
+ assertSame(RoundedCornerEnforcement.findBackground(mockWidgetView), mockWidgetView)
+ }
+
+ @Test
+ fun `Widget opted out of rounded corner enforcement`() {
+ val mockView = mock(View::class.java)
+
+ doReturn(android.R.id.background).whenever(mockView).id
+ doReturn(true).whenever(mockView).clipToOutline
+
+ assertTrue(RoundedCornerEnforcement.hasAppWidgetOptedOut(mockView))
+ }
+
+ @Test
+ fun `Compute rect based on widget view with background`() {
+ val mockBackgroundView = mock(View::class.java)
+ val mockWidgetView = mock(ViewGroup::class.java)
+ val testRect = Rect(0, 0, 0, 0)
+
+ doReturn(WIDTH).whenever(mockBackgroundView).width
+ doReturn(HEIGHT).whenever(mockBackgroundView).height
+ doReturn(LEFT).whenever(mockBackgroundView).left
+ doReturn(TOP).whenever(mockBackgroundView).top
+ doReturn(mockWidgetView).whenever(mockBackgroundView).parent
+
+ RoundedCornerEnforcement.computeRoundedRectangle(
+ mockWidgetView,
+ mockBackgroundView,
+ testRect
+ )
+
+ assertEquals(Rect(50, 75, 250, 275), testRect)
+ }
+
+ @Test
+ fun `Compute system radius`() {
+ val mockContext = mock(Context::class.java)
+ val mockRes = mock(Resources::class.java)
+
+ doReturn(mockRes).whenever(mockContext).resources
+ doReturn(RADIUS)
+ .whenever(mockRes)
+ .getDimension(eq(android.R.dimen.system_app_widget_background_radius))
+ doReturn(LAUNCHER_RADIUS)
+ .whenever(mockRes)
+ .getDimension(eq(R.dimen.enforced_rounded_corner_max_radius))
+
+ assertEquals(RADIUS, RoundedCornerEnforcement.computeEnforcedRadius(mockContext))
+ }
+
+ companion object {
+ const val WIDTH = 200
+ const val HEIGHT = 200
+ const val LEFT = 50
+ const val TOP = 75
+
+ const val RADIUS = 8f
+ const val LAUNCHER_RADIUS = 16f
+ }
+}
diff --git a/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt b/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
new file mode 100644
index 0000000..b491f17
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2024 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.folder
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.folder.FolderNameInfos.*
+import org.junit.Test
+import org.junit.runner.RunWith
+
+data class Label(val index: Int, val label: String, val score: Float)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderNameInfosTest {
+
+ companion object {
+ val statusList =
+ listOf(
+ SUCCESS,
+ HAS_PRIMARY,
+ HAS_SUGGESTIONS,
+ ERROR_NO_PROVIDER,
+ ERROR_APP_LOOKUP_FAILED,
+ ERROR_ALL_APP_LOOKUP_FAILED,
+ ERROR_NO_LABELS_GENERATED,
+ ERROR_LABEL_LOOKUP_FAILED,
+ ERROR_ALL_LABEL_LOOKUP_FAILED,
+ ERROR_NO_PACKAGES,
+ )
+ }
+
+ @Test
+ fun status() {
+ assertStatus(statusList)
+ assertStatus(
+ listOf(
+ ERROR_NO_PROVIDER,
+ ERROR_APP_LOOKUP_FAILED,
+ ERROR_ALL_APP_LOOKUP_FAILED,
+ ERROR_NO_LABELS_GENERATED,
+ ERROR_LABEL_LOOKUP_FAILED,
+ ERROR_ALL_LABEL_LOOKUP_FAILED,
+ ERROR_NO_PACKAGES,
+ )
+ )
+ assertStatus(
+ listOf(
+ SUCCESS,
+ HAS_PRIMARY,
+ HAS_SUGGESTIONS,
+ )
+ )
+ assertStatus(
+ listOf(
+ SUCCESS,
+ HAS_PRIMARY,
+ HAS_SUGGESTIONS,
+ )
+ )
+ }
+
+ fun assertStatus(statusList: List<Int>) {
+ var infos = FolderNameInfos()
+ statusList.forEach { infos.setStatus(it) }
+ assert(infos.status() == statusList.sum()) {
+ "There is an overlap on the status constants!"
+ }
+ }
+
+ @Test
+ fun hasPrimary() {
+ assertHasPrimary(
+ createNameInfos(listOf(Label(0, "label", 1f)), statusList),
+ hasPrimary = true
+ )
+ assertHasPrimary(
+ createNameInfos(listOf(Label(1, "label", 1f)), statusList),
+ hasPrimary = false
+ )
+ assertHasPrimary(
+ createNameInfos(
+ listOf(Label(0, "label", 1f)),
+ listOf(
+ ERROR_NO_PROVIDER,
+ ERROR_APP_LOOKUP_FAILED,
+ ERROR_ALL_APP_LOOKUP_FAILED,
+ ERROR_NO_LABELS_GENERATED,
+ ERROR_LABEL_LOOKUP_FAILED,
+ ERROR_ALL_LABEL_LOOKUP_FAILED,
+ ERROR_NO_PACKAGES,
+ )
+ ),
+ hasPrimary = false
+ )
+ }
+
+ private fun assertHasPrimary(nameInfos: FolderNameInfos, hasPrimary: Boolean) =
+ assert(nameInfos.hasPrimary() == hasPrimary)
+
+ private fun createNameInfos(labels: List<Label>?, statusList: List<Int>?): FolderNameInfos {
+ val infos = FolderNameInfos()
+ labels?.forEach { infos.setLabel(it.index, it.label, it.score) }
+ statusList?.forEach { infos.setStatus(it) }
+ return infos
+ }
+
+ @Test
+ fun hasSuggestions() {
+ assertHasSuggestions(
+ createNameInfos(listOf(Label(0, "label", 1f)), null),
+ hasSuggestions = true
+ )
+ assertHasSuggestions(createNameInfos(null, null), hasSuggestions = false)
+ // There is a max of 4 suggestions
+ assertHasSuggestions(
+ createNameInfos(listOf(Label(5, "label", 1f)), null),
+ hasSuggestions = false
+ )
+ assertHasSuggestions(
+ createNameInfos(
+ listOf(
+ Label(0, "label", 1f),
+ Label(1, "label", 1f),
+ Label(2, "label", 1f),
+ Label(3, "label", 1f)
+ ),
+ null
+ ),
+ hasSuggestions = true
+ )
+ }
+
+ private fun assertHasSuggestions(nameInfos: FolderNameInfos, hasSuggestions: Boolean) =
+ assert(nameInfos.hasSuggestions() == hasSuggestions)
+
+ @Test
+ fun hasContains() {
+ assertContains(
+ createNameInfos(
+ listOf(
+ Label(0, "label1", 1f),
+ Label(1, "label2", 1f),
+ Label(2, "label3", 1f),
+ Label(3, "label4", 1f)
+ ),
+ null
+ ),
+ label = Label(-1, "label3", -1f),
+ contains = true
+ )
+ assertContains(
+ createNameInfos(
+ listOf(
+ Label(0, "label1", 1f),
+ Label(1, "label2", 1f),
+ Label(2, "label3", 1f),
+ Label(3, "label4", 1f)
+ ),
+ null
+ ),
+ label = Label(-1, "label5", -1f),
+ contains = false
+ )
+ assertContains(
+ createNameInfos(null, null),
+ label = Label(-1, "label1", -1f),
+ contains = false
+ )
+ assertContains(
+ createNameInfos(
+ listOf(
+ Label(0, "label1", 1f),
+ Label(1, "label2", 1f),
+ Label(2, "lAbel3", 1f),
+ Label(3, "lEbel4", 1f)
+ ),
+ null
+ ),
+ label = Label(-1, "LaBEl3", -1f),
+ contains = true
+ )
+ }
+
+ private fun assertContains(nameInfos: FolderNameInfos, label: Label, contains: Boolean) =
+ assert(nameInfos.contains(label.label) == contains)
+}
diff --git a/tests/src/com/android/launcher3/folder/FolderTest.kt b/tests/src/com/android/launcher3/folder/FolderTest.kt
index e1daa74..4eb335e 100644
--- a/tests/src/com/android/launcher3/folder/FolderTest.kt
+++ b/tests/src/com/android/launcher3/folder/FolderTest.kt
@@ -18,23 +18,56 @@
import android.content.Context
import android.graphics.Point
+import android.view.KeyEvent
import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.TextView
+import androidx.core.view.isVisible
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.launcher3.Alarm
+import com.android.launcher3.DragSource
import com.android.launcher3.DropTarget.DragObject
import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.OnAlarmListener
+import com.android.launcher3.R
import com.android.launcher3.celllayout.board.FolderPoint
import com.android.launcher3.celllayout.board.TestWorkspaceBuilder
+import com.android.launcher3.dragndrop.DragController
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.dragndrop.DragView
+import com.android.launcher3.folder.Folder.MIN_CONTENT_DIMEN
+import com.android.launcher3.folder.Folder.ON_EXIT_CLOSE_DELAY
+import com.android.launcher3.folder.Folder.SCROLL_LEFT
+import com.android.launcher3.folder.Folder.SCROLL_NONE
+import com.android.launcher3.folder.Folder.STATE_ANIMATING
+import com.android.launcher3.folder.Folder.STATE_CLOSED
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.util.ActivityContextWrapper
import com.android.launcher3.util.ModelTestExtensions.clearModelDb
+import java.util.ArrayList
import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
+import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
/** Tests for [Folder] */
@SmallTest
@@ -44,7 +77,7 @@
private val context: Context =
ActivityContextWrapper(ApplicationProvider.getApplicationContext())
private val workspaceBuilder = TestWorkspaceBuilder(context)
- private val folder: Folder = Mockito.spy(Folder(context, null))
+ private val folder: Folder = spy(Folder(context, null))
@After
fun tearDown() {
@@ -60,8 +93,10 @@
folder.mContent = Mockito.mock(FolderPagedView::class.java)
val dragLayout = Mockito.mock(View::class.java)
val dragObject = Mockito.mock(DragObject::class.java)
- assertEquals(folder.deleteFolderOnDropCompleted, false)
+ folder.deleteFolderOnDropCompleted = false
+
folder.onDropCompleted(dragLayout, dragObject, true)
+
verify(folder, times(1)).replaceFolderWithFinalItem()
assertEquals(folder.deleteFolderOnDropCompleted, false)
}
@@ -74,12 +109,819 @@
folder.mContent = Mockito.mock(FolderPagedView::class.java)
val dragLayout = Mockito.mock(View::class.java)
val dragObject = Mockito.mock(DragObject::class.java)
- assertEquals(folder.deleteFolderOnDropCompleted, false)
+ folder.deleteFolderOnDropCompleted = false
+
folder.onDropCompleted(dragLayout, dragObject, true)
+
verify(folder, times(0)).replaceFolderWithFinalItem()
assertEquals(folder.deleteFolderOnDropCompleted, false)
}
+ @Test
+ fun `Test that we accept valid item type ITEM_TYPE_APPLICATION`() {
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_APPLICATION
+
+ val willAcceptResult = Folder.willAccept(itemInfo)
+
+ assertTrue(willAcceptResult)
+ }
+
+ @Test
+ fun `Test that we accept valid item type ITEM_TYPE_DEEP_SHORTCUT`() {
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+
+ val willAcceptResult = Folder.willAccept(itemInfo)
+
+ assertTrue(willAcceptResult)
+ }
+
+ @Test
+ fun `Test that we accept valid item type ITEM_TYPE_APP_PAIR`() {
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_APP_PAIR
+
+ val willAcceptResult = Folder.willAccept(itemInfo)
+
+ assertTrue(willAcceptResult)
+ }
+
+ @Test
+ fun `Test that we do not accept invalid item type ITEM_TYPE_APPWIDGET`() {
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_APPWIDGET
+
+ val willAcceptResult = Folder.willAccept(itemInfo)
+
+ assertFalse(willAcceptResult)
+ }
+
+ @Test
+ fun `Test that we do not accept invalid item type ITEM_TYPE_FOLDER`() {
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_FOLDER
+
+ val willAcceptResult = Folder.willAccept(itemInfo)
+
+ assertFalse(willAcceptResult)
+ }
+
+ @Test
+ fun `We should not animate open if items is null or less than or equal to 1`() {
+ folder.mInfo = Mockito.mock(FolderInfo::class.java)
+ val shouldAnimateOpenResult = folder.shouldAnimateOpen(null)
+
+ assertFalse(shouldAnimateOpenResult)
+ assertFalse(
+ folder.shouldAnimateOpen(arrayListOf<ItemInfo>(Mockito.mock(ItemInfo::class.java)))
+ )
+ }
+
+ @Test
+ fun `We should animate open if items greater than 1`() {
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = folderInfo
+
+ val shouldAnimateOpenResult = folder.shouldAnimateOpen(folder.mInfo.getContents())
+
+ assertTrue(shouldAnimateOpenResult)
+ }
+
+ @Test
+ fun `Should be true if there is an open folder`() {
+ val closeOpenFolderResult = folder.closeOpenFolder(Mockito.mock(Folder::class.java))
+
+ assertTrue(closeOpenFolderResult)
+ }
+
+ @Test
+ fun `Should be false if the open folder is this folder`() {
+ val closeOpenFolderResult = folder.closeOpenFolder(folder)
+
+ assertFalse(closeOpenFolderResult)
+ }
+
+ @Test
+ fun `Should be false if there is not an open folder`() {
+ val closeOpenFolderResult = folder.closeOpenFolder(null)
+
+ assertFalse(closeOpenFolderResult)
+ }
+
+ @Test
+ fun `If drag is in progress we should set mItemAddedBackToSelfViaIcon to true`() {
+ folder.itemAddedBackToSelfViaIcon = false
+ folder.isDragInProgress = true
+
+ folder.notifyDrop()
+
+ assertTrue(folder.itemAddedBackToSelfViaIcon)
+ }
+
+ @Test
+ fun `If drag is not in progress we should not set mItemAddedBackToSelfViaIcon to true`() {
+ folder.itemAddedBackToSelfViaIcon = false
+ folder.isDragInProgress = false
+
+ folder.notifyDrop()
+
+ assertFalse(folder.itemAddedBackToSelfViaIcon)
+ }
+
+ @Test
+ fun `If launcher dragging is not enabled onLongClick should return true`() {
+ `when`(folder.isLauncherDraggingEnabled).thenReturn(false)
+
+ val onLongClickResult = folder.onLongClick(Mockito.mock(View::class.java))
+
+ assertTrue(onLongClickResult)
+ }
+
+ @Test
+ fun `If launcher dragging is enabled we should return startDrag result`() {
+ `when`(folder.isLauncherDraggingEnabled).thenReturn(true)
+ val viewMock = Mockito.mock(View::class.java)
+ val dragOptions = Mockito.mock(DragOptions::class.java)
+
+ val onLongClickResult = folder.onLongClick(viewMock)
+
+ assertEquals(onLongClickResult, folder.startDrag(viewMock, dragOptions))
+ verify(folder, times(1)).startDrag(viewMock, dragOptions)
+ }
+
+ @Test
+ fun `Verify start drag works as intended when view is instanceof ItemInfo`() {
+ val itemInfo = ItemInfo()
+ itemInfo.rank = 5
+ val viewMock = Mockito.mock(View::class.java)
+ val dragOptions = DragOptions()
+ `when`(viewMock.tag).thenReturn(itemInfo)
+ folder.dragController = Mockito.mock(DragController::class.java)
+
+ folder.startDrag(viewMock, dragOptions)
+
+ assertEquals(folder.mEmptyCellRank, 5)
+ assertEquals(folder.currentDragView, viewMock)
+ verify(folder, times(1)).addDragListener(dragOptions)
+ verify(folder, times(1)).callBeginDragShared(viewMock, dragOptions)
+ }
+
+ @Test
+ fun `Verify start drag works as intended when view is not instanceof ItemInfo`() {
+ val viewMock = Mockito.mock(View::class.java)
+ val dragOptions = DragOptions()
+
+ folder.startDrag(viewMock, dragOptions)
+
+ verify(folder, times(0)).addDragListener(dragOptions)
+ verify(folder, times(0)).callBeginDragShared(viewMock, dragOptions)
+ }
+
+ @Test
+ fun `Verify that onDragStart has an effect if dragSource is this folder`() {
+ folder.itemsInvalidated = false
+ folder.isDragInProgress = false
+ folder.itemAddedBackToSelfViaIcon = true
+ folder.currentDragView = Mockito.mock(View::class.java)
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = spy(folderInfo)
+ val dragObject = DragObject(context)
+ dragObject.dragInfo = Mockito.mock(ItemInfo::class.java)
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ dragObject.dragSource = folder
+
+ folder.onDragStart(dragObject, DragOptions())
+
+ verify(folder.mContent, times(1)).removeItem(folder.currentDragView)
+ verify(folder.mInfo, times(1)).remove(dragObject.dragInfo, true)
+ assertTrue(folder.itemsInvalidated)
+ assertTrue(folder.isDragInProgress)
+ assertFalse(folder.itemAddedBackToSelfViaIcon)
+ }
+
+ @Test
+ fun `Verify that onDragStart has no effects if dragSource is not this folder`() {
+ folder.itemsInvalidated = false
+ folder.isDragInProgress = false
+ folder.itemAddedBackToSelfViaIcon = true
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val dragObject = DragObject(context)
+ dragObject.dragSource = Mockito.mock(DragSource::class.java)
+
+ folder.onDragStart(dragObject, DragOptions())
+
+ verify(folder.mContent, times(0)).removeItem(folder.currentDragView)
+ assertFalse(folder.itemsInvalidated)
+ assertFalse(folder.isDragInProgress)
+ assertTrue(folder.itemAddedBackToSelfViaIcon)
+ }
+
+ @Test
+ fun `Verify onDragEnd that we call completeDragExit and set drag in progress false`() {
+ doNothing().`when`(folder).completeDragExit()
+ folder.isExternalDrag = true
+ folder.isDragInProgress = true
+ folder.dragController = Mockito.mock(DragController::class.java)
+
+ folder.onDragEnd()
+
+ verify(folder, times(1)).completeDragExit()
+ verify(folder.dragController, times(1)).removeDragListener(folder)
+ assertFalse(folder.isDragInProgress)
+ }
+
+ @Test
+ fun `Verify onDragEnd that we do not call completeDragExit and set drag in progress false`() {
+ folder.isExternalDrag = false
+ folder.isDragInProgress = true
+ folder.dragController = Mockito.mock(DragController::class.java)
+
+ folder.onDragEnd()
+
+ verify(folder, times(0)).completeDragExit()
+ verify(folder.dragController, times(1)).removeDragListener(folder)
+ assertFalse(folder.isDragInProgress)
+ }
+
+ @Test
+ fun `startEditingFolderName should set hint to empty and showLabelSuggestions`() {
+ doNothing().`when`(folder).showLabelSuggestions()
+ folder.isEditingName = false
+ folder.folderName = FolderNameEditText(context)
+ folder.folderName.hint = "hello"
+
+ folder.startEditingFolderName()
+
+ verify(folder, times(1)).showLabelSuggestions()
+ assertEquals("", folder.folderName.hint)
+ assertTrue(folder.isEditingName)
+ }
+
+ @Test
+ fun `Ensure we set the title and hint correctly onBackKey when we have a new title`() {
+ val expectedHint = null
+ val expectedTitle = "hello"
+ folder.isEditingName = true
+ folder.folderName = spy(FolderNameEditText(context))
+ folder.folderName.setText(expectedTitle)
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = spy(folderInfo)
+ folder.mInfo.title = "world"
+ folder.mFolderIcon = Mockito.mock(FolderIcon::class.java)
+
+ folder.onBackKey()
+
+ assertEquals(expectedTitle, folder.mInfo.title)
+ verify(folder.mFolderIcon, times(1)).onTitleChanged(expectedTitle)
+ assertEquals(expectedHint, folder.folderName.hint)
+ assertFalse(folder.isEditingName)
+ verify(folder.folderName, times(1)).clearFocus()
+ }
+
+ @Test
+ fun `Ensure we set the title and hint correctly onBackKey when we do not have a new title`() {
+ val expectedHint = context.getString(R.string.folder_hint_text)
+ val expectedTitle = ""
+ folder.isEditingName = true
+ folder.folderName = spy(FolderNameEditText(context))
+ folder.folderName.setText(expectedTitle)
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = spy(folderInfo)
+ folder.mInfo.title = "world"
+ folder.mFolderIcon = Mockito.mock(FolderIcon::class.java)
+
+ folder.onBackKey()
+
+ assertEquals(expectedTitle, folder.mInfo.title)
+ verify(folder.mFolderIcon, times(1)).onTitleChanged(expectedTitle)
+ assertEquals(expectedHint, folder.folderName.hint)
+ assertFalse(folder.isEditingName)
+ verify(folder.folderName, times(1)).clearFocus()
+ }
+
+ @Test
+ fun `ensure onEditorAction calls dispatchBackKey when actionId is IME_ACTION_DONE`() {
+ folder.folderName = Mockito.mock(FolderNameEditText::class.java)
+
+ val result =
+ folder.onEditorAction(
+ Mockito.mock(TextView::class.java),
+ EditorInfo.IME_ACTION_DONE,
+ Mockito.mock(KeyEvent::class.java)
+ )
+
+ assertTrue(result)
+ verify(folder.folderName, times(1)).dispatchBackKey()
+ }
+
+ @Test
+ fun `ensure onEditorAction does not call dispatchBackKey when actionId is not IME_ACTION_DONE`() {
+ folder.folderName = Mockito.mock(FolderNameEditText::class.java)
+
+ val result =
+ folder.onEditorAction(
+ Mockito.mock(TextView::class.java),
+ EditorInfo.IME_ACTION_NONE,
+ Mockito.mock(KeyEvent::class.java)
+ )
+
+ assertFalse(result)
+ verify(folder.folderName, times(0)).dispatchBackKey()
+ }
+
+ @Test
+ fun `in completeDragExit we close the folder when mIsOpen`() {
+ doNothing().`when`(folder).close(true)
+ folder.setIsOpen(true)
+ folder.rearrangeOnClose = false
+
+ folder.completeDragExit()
+
+ verify(folder, times(1)).close(true)
+ assertTrue(folder.rearrangeOnClose)
+ }
+
+ @Test
+ fun `in completeDragExit we want to rearrange on close when it is animating`() {
+ folder.setIsOpen(false)
+ folder.rearrangeOnClose = false
+ folder.state = STATE_ANIMATING
+
+ folder.completeDragExit()
+
+ verify(folder, times(0)).close(true)
+ assertTrue(folder.rearrangeOnClose)
+ }
+
+ @Test
+ fun `in completeDragExit we want to call rearrangeChildren and clearDragInfo when not open and not animating`() {
+ doNothing().`when`(folder).rearrangeChildren()
+ doNothing().`when`(folder).clearDragInfo()
+ folder.setIsOpen(false)
+ folder.rearrangeOnClose = false
+ folder.state = STATE_CLOSED
+
+ folder.completeDragExit()
+
+ verify(folder, times(0)).close(true)
+ assertFalse(folder.rearrangeOnClose)
+ verify(folder, times(1)).rearrangeChildren()
+ verify(folder, times(1)).clearDragInfo()
+ }
+
+ @Test
+ fun `clearDragInfo should set current drag view to null and isExternalDrag to false`() {
+ folder.currentDragView = Mockito.mock(DragView::class.java)
+ folder.isExternalDrag = true
+
+ folder.clearDragInfo()
+
+ assertNull(folder.currentDragView)
+ assertFalse(folder.isExternalDrag)
+ }
+
+ @Test
+ fun `onDragExit should set alarm if drag is not complete`() {
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ dragObject.dragComplete = false
+
+ folder.onDragExit(dragObject)
+
+ verify(folder.onExitAlarm, times(1)).setOnAlarmListener(folder.mOnExitAlarmListener)
+ verify(folder.onExitAlarm, times(1)).setAlarm(ON_EXIT_CLOSE_DELAY.toLong())
+ }
+
+ @Test
+ fun `onDragExit should not set alarm if drag is complete`() {
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ dragObject.dragComplete = true
+
+ folder.onDragExit(dragObject)
+
+ verify(folder.onExitAlarm, times(0)).setOnAlarmListener(folder.mOnExitAlarmListener)
+ verify(folder.onExitAlarm, times(0)).setAlarm(ON_EXIT_CLOSE_DELAY.toLong())
+ }
+
+ @Test
+ fun `onDragExit should not clear scroll hint if already SCROLL_NONE`() {
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ folder.scrollHintDir = SCROLL_NONE
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+
+ folder.onDragExit(dragObject)
+
+ verify(folder.mContent, times(0)).clearScrollHint()
+ }
+
+ @Test
+ fun `onDragExit should clear scroll hint if not SCROLL_NONE and then set scroll hint to scroll none`() {
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ folder.scrollHintDir = SCROLL_LEFT
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+
+ folder.onDragExit(dragObject)
+
+ verify(folder.mContent, times(1)).clearScrollHint()
+ assertEquals(folder.scrollHintDir, SCROLL_NONE)
+ }
+
+ @Test
+ fun `onDragExit we should cancel reorder pause and hint alarms`() {
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ folder.scrollHintDir = SCROLL_NONE
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+ folder.onScrollHintAlarm = Mockito.mock(Alarm::class.java)
+ folder.scrollPauseAlarm = Mockito.mock(Alarm::class.java)
+
+ folder.onDragExit(dragObject)
+
+ verify(folder.reorderAlarm, times(1)).cancelAlarm()
+ verify(folder.onScrollHintAlarm, times(1)).cancelAlarm()
+ verify(folder.scrollPauseAlarm, times(1)).cancelAlarm()
+ assertEquals(folder.scrollHintDir, SCROLL_NONE)
+ }
+
+ @Test
+ fun `when calling prepareAccessibilityDrop we should cancel pending reorder alarm and call onAlarm`() {
+ folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+ folder.mReorderAlarmListener = Mockito.mock(OnAlarmListener::class.java)
+ `when`(folder.reorderAlarm.alarmPending()).thenReturn(true)
+
+ folder.prepareAccessibilityDrop()
+
+ verify(folder.reorderAlarm, times(1)).cancelAlarm()
+ verify(folder.mReorderAlarmListener, times(1)).onAlarm(folder.reorderAlarm)
+ }
+
+ @Test
+ fun `when calling prepareAccessibilityDrop we should not do anything if there is no pending alarm`() {
+ folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+ folder.mReorderAlarmListener = Mockito.mock(OnAlarmListener::class.java)
+ `when`(folder.reorderAlarm.alarmPending()).thenReturn(false)
+
+ folder.prepareAccessibilityDrop()
+
+ verify(folder.reorderAlarm, times(0)).cancelAlarm()
+ verify(folder.mReorderAlarmListener, times(0)).onAlarm(folder.reorderAlarm)
+ }
+
+ @Test
+ fun `isDropEnabled should be true as long as state is not STATE_ANIMATING`() {
+ folder.state = STATE_CLOSED
+
+ val isDropEnabled = folder.isDropEnabled
+
+ assertTrue(isDropEnabled)
+ }
+
+ @Test
+ fun `isDropEnabled should be false if state is STATE_ANIMATING`() {
+ folder.state = STATE_ANIMATING
+
+ val isDropEnabled = folder.isDropEnabled
+
+ assertFalse(isDropEnabled)
+ }
+
+ @Test
+ fun `getItemCount should return the number of items in the folder`() {
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = folderInfo
+
+ val itemCount = folder.itemCount
+
+ assertEquals(itemCount, 2)
+ }
+
+ @Test
+ fun `hideItem should set the visibility of the corresponding ItemInfo to invisible`() {
+ val itemInfo = ItemInfo()
+ val view = View(context)
+ view.isVisible = true
+ doReturn(view).whenever(folder).getViewForInfo(itemInfo)
+
+ folder.hideItem(itemInfo)
+
+ assertFalse(view.isVisible)
+ }
+
+ @Test
+ fun `showItem should set the visibility of the corresponding ItemInfo to visible`() {
+ val itemInfo = ItemInfo()
+ val view = View(context)
+ view.isVisible = false
+ doReturn(view).whenever(folder).getViewForInfo(itemInfo)
+
+ folder.showItem(itemInfo)
+
+ assertTrue(view.isVisible)
+ }
+
+ @Test
+ fun `onDragEnter should cancel exit alarm and set the scroll area offset to dragRegionWidth divided by two minus xOffset`() {
+ folder.mPrevTargetRank = 1
+ val dragObject = Mockito.mock(DragObject::class.java)
+ val dragView = Mockito.mock(DragView::class.java)
+ dragObject.dragView = dragView
+ folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+ `when`(dragObject.dragView.getDragRegionWidth()).thenReturn(100)
+ dragObject.xOffset = 20
+
+ folder.onDragEnter(dragObject)
+
+ verify(folder.onExitAlarm, times(1)).cancelAlarm()
+ assertEquals(-1, folder.mPrevTargetRank)
+ assertEquals(30, folder.scrollAreaOffset)
+ }
+
+ @Test
+ fun `acceptDrop should return true with the correct item type as a parameter`() {
+ val dragObject = Mockito.mock(DragObject::class.java)
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_APP_PAIR
+ dragObject.dragInfo = itemInfo
+
+ val result = folder.acceptDrop(dragObject)
+
+ assertTrue(result)
+ }
+
+ @Test
+ fun `acceptDrop should return false with the incorrect item type as a parameter`() {
+ val dragObject = Mockito.mock(DragObject::class.java)
+ val itemInfo = Mockito.mock(ItemInfo::class.java)
+ itemInfo.itemType = ITEM_TYPE_APPWIDGET
+ dragObject.dragInfo = itemInfo
+
+ val result = folder.acceptDrop(dragObject)
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `rearrangeChildren should return early if content view are not bound`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ folder.itemsInvalidated = false
+ doReturn(false).whenever(folder.mContent).areViewsBound()
+
+ folder.rearrangeChildren()
+
+ verify(folder.mContent, times(0)).arrangeChildren(folder.iconsInReadingOrder)
+ assertFalse(folder.itemsInvalidated)
+ }
+
+ @Test
+ fun `rearrangeChildren should call arrange children and invalidate items`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ folder.itemsInvalidated = false
+ doReturn(true).whenever(folder.mContent).areViewsBound()
+ val iconsInReadingOrderList = ArrayList<View>()
+ `when`(folder.iconsInReadingOrder).thenReturn(iconsInReadingOrderList)
+ doNothing().`when`(folder.mContent).arrangeChildren(iconsInReadingOrderList)
+
+ folder.rearrangeChildren()
+
+ verify(folder.mContent, times(1)).arrangeChildren(folder.iconsInReadingOrder)
+ assertTrue(folder.itemsInvalidated)
+ }
+
+ @Test
+ fun `getItemCount should return the size of info getContents size`() {
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = folderInfo
+
+ val itemCount = folder.itemCount
+
+ assertEquals(2, itemCount)
+ }
+
+ @Test
+ fun `replaceFolderWithFinalItem should set mDestroyed to true if we replace folder with final item`() {
+ val launcherDelegate = Mockito.mock(LauncherDelegate::class.java)
+ folder.mLauncherDelegate = launcherDelegate
+ `when`(folder.mLauncherDelegate.replaceFolderWithFinalItem(folder)).thenReturn(true)
+
+ folder.replaceFolderWithFinalItem()
+
+ assertTrue(folder.isDestroyed)
+ }
+
+ @Test
+ fun `replaceFolderWithFinalItem should set mDestroyed to false if we do not replace folder with final item`() {
+ val launcherDelegate = Mockito.mock(LauncherDelegate::class.java)
+ folder.mLauncherDelegate = launcherDelegate
+ `when`(folder.mLauncherDelegate.replaceFolderWithFinalItem(folder)).thenReturn(false)
+
+ folder.replaceFolderWithFinalItem()
+
+ assertFalse(folder.isDestroyed)
+ }
+
+ @Test
+ fun `getContentAreaHeight should return maxContentAreaHeight`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredHeight).thenReturn(100)
+ `when`(folder.maxContentAreaHeight).thenReturn(50)
+
+ val height = folder.contentAreaHeight
+
+ assertEquals(50, height)
+ }
+
+ @Test
+ fun `getContentAreaHeight should return desiredHeight`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredHeight).thenReturn(50)
+ `when`(folder.maxContentAreaHeight).thenReturn(100)
+
+ val height = folder.contentAreaHeight
+
+ assertEquals(50, height)
+ }
+
+ @Test
+ fun `getContentAreaHeight should return MIN_CONTENT_DIMEN`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredHeight).thenReturn(1)
+ `when`(folder.maxContentAreaHeight).thenReturn(2)
+
+ val height = folder.contentAreaHeight
+
+ assertEquals(MIN_CONTENT_DIMEN, height)
+ }
+
+ @Test
+ fun `getContentAreaWidth should return desired width`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredWidth).thenReturn(50)
+
+ val width = folder.contentAreaWidth
+
+ assertEquals(50, width)
+ }
+
+ @Test
+ fun `getContentAreaWidth should return MIN_CONTENT_DIMEN`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredWidth).thenReturn(1)
+
+ val width = folder.contentAreaWidth
+
+ assertEquals(MIN_CONTENT_DIMEN, width)
+ }
+
+ @Test
+ fun `getFolderWidth should return padding left plus padding right plus desired width`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.mContent.desiredWidth).thenReturn(1)
+ `when`(folder.paddingLeft).thenReturn(10)
+ `when`(folder.paddingRight).thenReturn(10)
+
+ val width = folder.folderWidth
+
+ assertEquals(21, width)
+ }
+
+ @Test
+ fun `getFolderHeight with no params should return getFolderHeight`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.contentAreaHeight).thenReturn(100)
+ `when`(folder.getFolderHeight(folder.contentAreaHeight)).thenReturn(120)
+
+ val height = folder.folderHeight
+
+ assertEquals(120, height)
+ }
+
+ @Test
+ fun `getFolderWidth with contentAreaHeight should return padding top plus padding bottom plus contentAreaHeight plus footer height`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ `when`(folder.footerHeight).thenReturn(100)
+ `when`(folder.paddingTop).thenReturn(10)
+ `when`(folder.paddingBottom).thenReturn(10)
+
+ val height = folder.getFolderHeight(100)
+
+ assertEquals(220, height)
+ }
+
+ @Test
+ fun `onRemove should call removeItem with the correct views`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val items =
+ arrayListOf<ItemInfo>(
+ Mockito.mock(ItemInfo::class.java),
+ Mockito.mock(ItemInfo::class.java)
+ )
+ val view1 = Mockito.mock(View::class.java)
+ val view2 = Mockito.mock(View::class.java)
+ doReturn(view1).whenever(folder).getViewForInfo(items[0])
+ doReturn(view2).whenever(folder).getViewForInfo(items[1])
+ doReturn(2).whenever(folder).itemCount
+
+ folder.onRemove(items)
+
+ verify(folder.mContent, times(1)).removeItem(view1)
+ verify(folder.mContent, times(1)).removeItem(view2)
+ }
+
+ @Test
+ fun `onRemove should set mRearrangeOnClose to true and not call rearrangeChildren if animating`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ folder.state = STATE_ANIMATING
+ val items =
+ arrayListOf<ItemInfo>(
+ Mockito.mock(ItemInfo::class.java),
+ Mockito.mock(ItemInfo::class.java)
+ )
+ val view1 = Mockito.mock(View::class.java)
+ val view2 = Mockito.mock(View::class.java)
+ doReturn(view1).whenever(folder).getViewForInfo(items[0])
+ doReturn(view2).whenever(folder).getViewForInfo(items[1])
+ doReturn(2).whenever(folder).itemCount
+
+ folder.onRemove(items)
+
+ assertTrue(folder.rearrangeOnClose)
+ verify(folder, times(0)).rearrangeChildren()
+ }
+
+ @Test
+ fun `onRemove should set not change mRearrangeOnClose and not call rearrangeChildren if not animating`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ folder.state = STATE_CLOSED
+ folder.rearrangeOnClose = false
+ val items =
+ arrayListOf<ItemInfo>(
+ Mockito.mock(ItemInfo::class.java),
+ Mockito.mock(ItemInfo::class.java)
+ )
+ val view1 = Mockito.mock(View::class.java)
+ val view2 = Mockito.mock(View::class.java)
+ doReturn(view1).whenever(folder).getViewForInfo(items[0])
+ doReturn(view2).whenever(folder).getViewForInfo(items[1])
+ doReturn(2).whenever(folder).itemCount
+
+ folder.onRemove(items)
+
+ assertFalse(folder.rearrangeOnClose)
+ verify(folder, times(1)).rearrangeChildren()
+ }
+
+ @Test
+ fun `onRemove should call close if mIsOpen is true and item count is less than or equal to one`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val items =
+ arrayListOf<ItemInfo>(
+ Mockito.mock(ItemInfo::class.java),
+ Mockito.mock(ItemInfo::class.java)
+ )
+ val view1 = Mockito.mock(View::class.java)
+ val view2 = Mockito.mock(View::class.java)
+ doReturn(view1).whenever(folder).getViewForInfo(items[0])
+ doReturn(view2).whenever(folder).getViewForInfo(items[1])
+ doReturn(1).whenever(folder).itemCount
+ folder.setIsOpen(true)
+ doNothing().`when`(folder).close(true)
+
+ folder.onRemove(items)
+
+ verify(folder, times(1)).close(true)
+ }
+
+ @Test
+ fun `onRemove should call replaceFolderWithFinalItem if mIsOpen is false and item count is less than or equal to one`() {
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val items =
+ arrayListOf<ItemInfo>(
+ Mockito.mock(ItemInfo::class.java),
+ Mockito.mock(ItemInfo::class.java)
+ )
+ val view1 = Mockito.mock(View::class.java)
+ val view2 = Mockito.mock(View::class.java)
+ doReturn(view1).whenever(folder).getViewForInfo(items[0])
+ doReturn(view2).whenever(folder).getViewForInfo(items[1])
+ doReturn(1).whenever(folder).itemCount
+ folder.setIsOpen(false)
+
+ folder.onRemove(items)
+
+ verify(folder, times(1)).replaceFolderWithFinalItem()
+ }
+
companion object {
const val TWO_ICON_FOLDER_TYPE = 'A'
}
diff --git a/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
new file mode 100644
index 0000000..9a8f957
--- /dev/null
+++ b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 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.pageindicators
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.ActivityContextWrapper
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+import org.mockito.Mockito
+
+class PageIndicatorDotsTest {
+
+ private val context: Context =
+ ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+ private val pageIndicatorDots: PageIndicatorDots = Mockito.spy(PageIndicatorDots(context))
+
+ @Test
+ fun `setActiveMarker should set the active page to the parameter passed`() {
+ pageIndicatorDots.setActiveMarker(2)
+
+ assertEquals(2, pageIndicatorDots.activePage)
+ }
+
+ @Test
+ fun `setActiveMarker should set the active page to the parameter passed divided by two in two panel layouts`() {
+ pageIndicatorDots.mIsTwoPanels = true
+
+ pageIndicatorDots.setActiveMarker(5)
+
+ assertEquals(2, pageIndicatorDots.activePage)
+ }
+
+ @Test
+ fun `setMarkersCount should set the number of pages to the passed parameter and if the last page gets removed we want to go to the previous page`() {
+ pageIndicatorDots.setMarkersCount(3)
+
+ assertEquals(3, pageIndicatorDots.numPages)
+ }
+
+ @Test
+ fun `for setMarkersCount if the last page gets removed we want to go to the previous page`() {
+ pageIndicatorDots.setActiveMarker(2)
+
+ pageIndicatorDots.setMarkersCount(2)
+
+ assertEquals(1, pageIndicatorDots.activePage)
+ assertEquals(pageIndicatorDots.activePage.toFloat(), pageIndicatorDots.currentPosition)
+ }
+}