diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index 3d15e77853..ff97b22e4e 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -1,6 +1,13 @@ +[Builtin Hooks] +ktfmt = true + +[Builtin Hooks Options] +ktfmt = --kotlinlang-style + +[Tool Paths] +ktfmt = ${REPO_ROOT}/prebuilts/build-tools/common/framework/ktfmt.jar + [Hook Scripts] checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --config_xml tools/checkstyle.xml --sha ${PREUPLOAD_COMMIT} -ktfmt_hook = ${REPO_ROOT}/external/ktfmt/ktfmt.py --check ${PREUPLOAD_FILES} - flag_hook = ${REPO_ROOT}/frameworks/base/packages/SystemUI/flag_check.py --msg=${PREUPLOAD_COMMIT_MESSAGE} --files=${PREUPLOAD_FILES} --project=${REPO_PATH} diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java index 2625919ec0..fa80dc2776 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java @@ -101,6 +101,11 @@ public class AllAppsState extends LauncherState { return launcher.getAppsView().getDescription(); } + @Override + public int getTitle() { + return R.string.all_apps_label; + } + @Override public float getVerticalProgress(Launcher launcher) { return 0f; diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java index 7173298d19..6822f1b39c 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java @@ -182,6 +182,11 @@ public class OverviewState extends LauncherState { return launcher.getString(R.string.accessibility_recent_apps); } + @Override + public int getTitle() { + return R.string.accessibility_recent_apps; + } + public static float getDefaultSwipeHeight(Launcher launcher) { return LayoutUtils.getDefaultSwipeHeight(launcher, launcher.getDeviceProfile()); } diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java index dfbae657dd..b4b8c5b82c 100644 --- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java +++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java @@ -198,6 +198,12 @@ public class QuickstepTestInformationHandler extends TestInformationHandler { .unstashBubbleBarIfStashed(); }); return response; + case TestProtocol.REQUEST_INJECT_FAKE_TRACKPAD: + runOnTISBinder(tisBinder -> tisBinder.injectFakeTrackpadForTesting()); + return response; + case TestProtocol.REQUEST_EJECT_FAKE_TRACKPAD: + runOnTISBinder(tisBinder -> tisBinder.ejectFakeTrackpadForTesting()); + return response; } return super.call(method, arg, extras); diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java index 97a0b3fd23..3df62dacae 100644 --- a/quickstep/src/com/android/quickstep/RecentsActivity.java +++ b/quickstep/src/com/android/quickstep/RecentsActivity.java @@ -371,6 +371,9 @@ public final class RecentsActivity extends StatefulActivity implem getSystemUiController().updateUiState(SystemUiController.UI_STATE_BASE_WINDOW, Themes.getAttrBoolean(this, R.attr.isWorkspaceDarkText)); ACTIVITY_TRACKER.handleCreate(this); + + // Set screen title for Talkback + setTitle(R.string.accessibility_recent_apps); } @Override diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index 4599f180b1..58bb8fc7c4 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -68,11 +68,13 @@ import android.content.IIntentSender; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Region; +import android.hardware.input.InputManager; import android.os.Bundle; import android.os.IBinder; import android.os.Looper; import android.os.SystemClock; import android.os.Trace; +import android.util.ArraySet; import android.util.Log; import android.view.Choreographer; import android.view.InputDevice; @@ -83,6 +85,7 @@ import androidx.annotation.BinderThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.ConstantItem; @@ -146,6 +149,7 @@ import com.android.wm.shell.startingsurface.IStartingWindow; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -396,6 +400,25 @@ public class TouchInteractionService extends Service { return tis.mTaskbarManager; } + @VisibleForTesting + public void injectFakeTrackpadForTesting() { + TouchInteractionService tis = mTis.get(); + if (tis == null) return; + tis.mTrackpadsConnected.add(1000); + tis.initInputMonitor("tapl testing"); + } + + @VisibleForTesting + public void ejectFakeTrackpadForTesting() { + TouchInteractionService tis = mTis.get(); + if (tis == null) return; + tis.mTrackpadsConnected.clear(); + // This method destroys the current input monitor if set up, and only init a new one + // in 3-button mode if {@code mTrackpadsConnected} is not empty. So in other words, + // it will destroy the input monitor. + tis.initInputMonitor("tapl testing"); + } + /** * Sets whether a predictive back-to-home animation is in progress in the device state */ @@ -453,6 +476,47 @@ public class TouchInteractionService extends Service { } } + private final InputManager.InputDeviceListener mInputDeviceListener = + new InputManager.InputDeviceListener() { + @Override + public void onInputDeviceAdded(int deviceId) { + if (isTrackpadDevice(deviceId)) { + boolean wasEmpty = mTrackpadsConnected.isEmpty(); + mTrackpadsConnected.add(deviceId); + if (wasEmpty) { + update(); + } + } + } + + @Override + public void onInputDeviceChanged(int deviceId) { + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + mTrackpadsConnected.remove(deviceId); + if (mTrackpadsConnected.isEmpty()) { + update(); + } + } + + private void update() { + if (mInputMonitorCompat != null && !mTrackpadsConnected.isEmpty()) { + // Don't destroy and reinitialize input monitor due to trackpad + // connecting when it's already set up. + return; + } + initInputMonitor("onTrackpadConnected()"); + } + + private boolean isTrackpadDevice(int deviceId) { + InputDevice inputDevice = mInputManager.getInputDevice(deviceId); + return inputDevice.getSources() == (InputDevice.SOURCE_MOUSE + | InputDevice.SOURCE_TOUCHPAD); + } + }; + private static boolean sConnected = false; private static boolean sIsInitialized = false; private RotationTouchHelper mRotationTouchHelper; @@ -503,6 +567,8 @@ public class TouchInteractionService extends Service { private TaskbarManager mTaskbarManager; private Function mSwipeUpProxyProvider = i -> null; private AllAppsActionManager mAllAppsActionManager; + private InputManager mInputManager; + private final Set mTrackpadsConnected = new ArraySet<>(); @Override public void onCreate() { @@ -514,6 +580,15 @@ public class TouchInteractionService extends Service { mDeviceState = new RecentsAnimationDeviceState(this, true); mAllAppsActionManager = new AllAppsActionManager( this, UI_HELPER_EXECUTOR, this::createAllAppsPendingIntent); + mInputManager = getSystemService(InputManager.class); + if (ENABLE_TRACKPAD_GESTURE.get()) { + mInputManager.registerInputDeviceListener(mInputDeviceListener, + UI_HELPER_EXECUTOR.getHandler()); + int [] inputDevices = mInputManager.getInputDeviceIds(); + for (int inputDeviceId : inputDevices) { + mInputDeviceListener.onInputDeviceAdded(inputDeviceId); + } + } mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks); mRotationTouchHelper = mDeviceState.getRotationTouchHelper(); mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer(); @@ -542,7 +617,8 @@ public class TouchInteractionService extends Service { private void initInputMonitor(String reason) { disposeEventHandlers("Initializing input monitor due to: " + reason); - if (mDeviceState.isButtonNavMode() && !ENABLE_TRACKPAD_GESTURE.get()) { + if (mDeviceState.isButtonNavMode() && (!ENABLE_TRACKPAD_GESTURE.get() + || mTrackpadsConnected.isEmpty())) { return; } @@ -678,6 +754,9 @@ public class TouchInteractionService extends Service { mAllAppsActionManager.onDestroy(); + mInputManager.unregisterInputDeviceListener(mInputDeviceListener); + mTrackpadsConnected.clear(); + mTaskbarManager.destroy(); sConnected = false; diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java index a8ebe51b8a..cb1ee0c804 100644 --- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java +++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java @@ -74,7 +74,7 @@ public final class DigitalWellBeingToast { SPLIT_GRID_BANNER_SMALL, }) @Retention(RetentionPolicy.SOURCE) - @interface SPLIT_BANNER_CONFIG{} + @interface SplitBannerConfig{} static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS); static final int MINUTE_MS = 60000; @@ -88,7 +88,6 @@ public final class DigitalWellBeingToast { private Task mTask; private boolean mHasLimit; - private long mAppUsageLimitTimeMs; private long mAppRemainingTimeMs; @Nullable private View mBanner; @@ -96,10 +95,11 @@ public final class DigitalWellBeingToast { private float mBannerOffsetPercentage; @Nullable private SplitBounds mSplitBounds; - private int mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN; private float mSplitOffsetTranslationY; private float mSplitOffsetTranslationX; + private boolean mIsDestroyed = false; + public DigitalWellBeingToast(RecentsViewContainer container, TaskView taskView) { mContainer = container; mTaskView = taskView; @@ -110,12 +110,10 @@ public final class DigitalWellBeingToast { mHasLimit = false; mTaskView.setContentDescription(mTask.titleDescription); replaceBanner(null); - mAppUsageLimitTimeMs = -1; mAppRemainingTimeMs = -1; } private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) { - mAppUsageLimitTimeMs = appUsageLimitTimeMs; mAppRemainingTimeMs = appRemainingTimeMs; mHasLimit = true; TextView toast = mContainer.getViewCache().getView(R.layout.digital_wellbeing_toast, @@ -138,89 +136,95 @@ public final class DigitalWellBeingToast { } public void initialize(Task task) { - mAppUsageLimitTimeMs = mAppRemainingTimeMs = -1; + if (mIsDestroyed) { + throw new IllegalStateException("Cannot re-initialize a destroyed toast"); + } mTask = task; ORDERED_BG_EXECUTOR.execute(() -> { - AppUsageLimit usageLimit = null; - try { - usageLimit = mLauncherApps.getAppUsageLimit( - mTask.getTopComponent().getPackageName(), - UserHandle.of(mTask.key.userId)); - } catch (Exception e) { - Log.e(TAG, "Error initializing digital well being toast", e); - } - final long appUsageLimitTimeMs = - usageLimit != null ? usageLimit.getTotalUsageLimit() : -1; - final long appRemainingTimeMs = - usageLimit != null ? usageLimit.getUsageRemaining() : -1; - - mTaskView.post(() -> { - if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) { - setNoLimit(); - } else { - setLimit(appUsageLimitTimeMs, appRemainingTimeMs); - } - }); + AppUsageLimit usageLimit = null; + try { + usageLimit = mLauncherApps.getAppUsageLimit( + mTask.getTopComponent().getPackageName(), + UserHandle.of(mTask.key.userId)); + } catch (Exception e) { + Log.e(TAG, "Error initializing digital well being toast", e); + } + final long appUsageLimitTimeMs = + usageLimit != null ? usageLimit.getTotalUsageLimit() : -1; + final long appRemainingTimeMs = + usageLimit != null ? usageLimit.getUsageRemaining() : -1; + mTaskView.post(() -> { + if (mIsDestroyed) { + return; } - ); + if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) { + setNoLimit(); + } else { + setLimit(appUsageLimitTimeMs, appRemainingTimeMs); + } + }); + }); } - public void setSplitConfiguration(SplitBounds splitBounds) { + /** + * Mark the DWB toast as destroyed and remove banner from TaskView. + */ + public void destroy() { + mIsDestroyed = true; + mTaskView.post(() -> replaceBanner(null)); + } + + public void setSplitBounds(@Nullable SplitBounds splitBounds) { mSplitBounds = splitBounds; + } + + private @SplitBannerConfig int getSplitBannerConfig() { if (mSplitBounds == null || !mContainer.getDeviceProfile().isTablet || mTaskView.isFocusedTask()) { - mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN; - return; + return SPLIT_BANNER_FULLSCREEN; } // For portrait grid only height of task changes, not width. So we keep the text the same if (!mContainer.getDeviceProfile().isLeftRightSplit) { - mSplitBannerConfig = SPLIT_GRID_BANNER_LARGE; - return; + return SPLIT_GRID_BANNER_LARGE; } // For landscape grid, for 30% width we only show icon, otherwise show icon and time if (mTask.key.id == mSplitBounds.leftTopTaskId) { - mSplitBannerConfig = mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY ? - SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; + return mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY + ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; } else { - mSplitBannerConfig = mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY ? - SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; + return mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY + ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE; } } private String getReadableDuration( Duration duration, - FormatWidth formatWidthHourAndMinute, - @StringRes int durationLessThanOneMinuteStringId, - boolean forceFormatWidth) { + @StringRes int durationLessThanOneMinuteStringId) { int hours = Math.toIntExact(duration.toHours()); int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes()); - // Apply formatWidthHourAndMinute if both the hour part and the minute part are non-zero. + // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero. if (hours > 0 && minutes > 0) { - return MeasureFormat.getInstance(Locale.getDefault(), formatWidthHourAndMinute) + return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.NARROW) .formatMeasures( new Measure(hours, MeasureUnit.HOUR), new Measure(minutes, MeasureUnit.MINUTE)); } - // Apply formatWidthHourOrMinute if only the hour part is non-zero (unless forced). + // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced). if (hours > 0) { - return MeasureFormat.getInstance( - Locale.getDefault(), - forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE) - .formatMeasures(new Measure(hours, MeasureUnit.HOUR)); + return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( + new Measure(hours, MeasureUnit.HOUR)); } - // Apply formatWidthHourOrMinute if only the minute part is non-zero (unless forced). + // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced). if (minutes > 0) { - return MeasureFormat.getInstance( - Locale.getDefault() - , forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE) - .formatMeasures(new Measure(minutes, MeasureUnit.MINUTE)); + return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( + new Measure(minutes, MeasureUnit.MINUTE)); } // Use a specific string for usage less than one minute but non-zero. @@ -229,13 +233,12 @@ public final class DigitalWellBeingToast { } // Otherwise, return 0-minute string. - return MeasureFormat.getInstance( - Locale.getDefault(), forceFormatWidth ? formatWidthHourAndMinute : FormatWidth.WIDE) - .formatMeasures(new Measure(0, MeasureUnit.MINUTE)); + return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures( + new Measure(0, MeasureUnit.MINUTE)); } /** - * Returns text to show for the banner depending on {@link #mSplitBannerConfig} + * Returns text to show for the banner depending on {@link #getSplitBannerConfig()} * If {@param forContentDesc} is {@code true}, this will always return the full * string corresponding to {@link #SPLIT_BANNER_FULLSCREEN} */ @@ -245,16 +248,16 @@ public final class DigitalWellBeingToast { (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS : remainingTime); String readableDuration = getReadableDuration(duration, - FormatWidth.NARROW, - R.string.shorter_duration_less_than_one_minute, - false /* forceFormatWidth */); - if (forContentDesc || mSplitBannerConfig == SPLIT_BANNER_FULLSCREEN) { + R.string.shorter_duration_less_than_one_minute + /* forceFormatWidth */); + @SplitBannerConfig int splitBannerConfig = getSplitBannerConfig(); + if (forContentDesc || splitBannerConfig == SPLIT_BANNER_FULLSCREEN) { return mContainer.asContext().getString( R.string.time_left_for_app, readableDuration); } - if (mSplitBannerConfig == SPLIT_GRID_BANNER_SMALL) { + if (splitBannerConfig == SPLIT_GRID_BANNER_SMALL) { // show no text return ""; } else { // SPLIT_GRID_BANNER_LARGE @@ -309,7 +312,7 @@ public final class DigitalWellBeingToast { private void setBanner(@Nullable View view) { mBanner = view; - if (view != null && mTaskView.getRecentsView() != null) { + if (mBanner != null && mTaskView.getRecentsView() != null) { setupAndAddBanner(); setBannerOutline(); } diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt index efbfa09420..d6a3376c53 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt @@ -162,6 +162,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT ) } + taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) } setOrientationState(orientedState) } @@ -240,6 +241,10 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) { splitBoundsConfig = splitBounds + taskContainers.forEach { + it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) + it.digitalWellBeingToast?.initialize(it.task) + } invalidate() } diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt index 4045ad7a91..71093af5dc 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.kt +++ b/quickstep/src/com/android/quickstep/views/TaskView.kt @@ -512,6 +512,7 @@ constructor( onTaskListVisibilityChanged(false) borderEnabled = false taskViewId = UNBOUND_TASK_VIEW_ID + taskContainers.forEach { it.destroy() } } // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. @@ -801,12 +802,12 @@ constructor( taskContainers.forEach { if (visible) { recentsModel.iconCache - .updateIconInBackground(it.task) { thumbnailData -> - setIcon(it.iconView, thumbnailData.icon) + .updateIconInBackground(it.task) { task -> + setIcon(it.iconView, task.icon) if (enableOverviewIconMenu()) { - setText(it.iconView, thumbnailData.title) + setText(it.iconView, task.title) } - it.digitalWellBeingToast?.initialize(thumbnailData) + it.digitalWellBeingToast?.initialize(task) } ?.also { request -> pendingIconLoadRequests.add(request) } } else { @@ -1586,6 +1587,11 @@ constructor( val taskView: TaskView get() = this@TaskView + fun destroy() { + digitalWellBeingToast?.destroy() + thumbnailView?.let { taskView.removeView(it) } + } + // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView fun bindThumbnailView() { diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java index e4f8b6cd43..2c23f867cb 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java @@ -37,6 +37,7 @@ import com.android.launcher3.util.rule.TestStabilityRule; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -47,8 +48,14 @@ public class TaplTestsTrackpad extends AbstractQuickStepTest { private static final String READ_DEVICE_CONFIG_PERMISSION = "android.permission.READ_DEVICE_CONFIG"; + @Before + public void setup() { + mLauncher.injectFakeTrackpad(); + } + @After public void tearDown() { + mLauncher.ejectFakeTrackpad(); mLauncher.setTrackpadGestureType(TrackpadGestureType.NONE); } @@ -110,8 +117,8 @@ public class TaplTestsTrackpad extends AbstractQuickStepTest { } @Test - @NavigationModeSwitch @PortraitLandscape + @NavigationModeSwitch public void testQuickSwitchFromHome() throws Exception { assumeTrue(mLauncher.isTablet()); diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 27411581a9..aa83c01a97 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -320,6 +320,8 @@ 52dp 12dp 6dp + + 70dp 16dp diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index b89d05e808..7f72526942 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -1268,11 +1268,7 @@ public class Launcher extends StatefulActivity } // Set screen title for Talkback - if (state == ALL_APPS) { - setTitle(R.string.all_apps_label); - } else { - setTitle(R.string.home_screen); - } + setTitle(state.getTitle()); } /** diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java index 72a3c53c7b..d2d56f2d8c 100644 --- a/src/com/android/launcher3/LauncherState.java +++ b/src/com/android/launcher3/LauncherState.java @@ -38,6 +38,7 @@ import android.view.View; import android.view.animation.Interpolator; import androidx.annotation.FloatRange; +import androidx.annotation.StringRes; import com.android.launcher3.statemanager.BaseState; import com.android.launcher3.statemanager.StateManager; @@ -369,6 +370,10 @@ public abstract class LauncherState implements BaseState { return launcher.getWorkspace().getCurrentPageDescription(); } + public @StringRes int getTitle() { + return R.string.home_screen; + } + public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) { if ((this != NORMAL && this != HINT_STATE) || !launcher.getDeviceProfile().shouldFadeAdjacentWorkspaceScreens()) { diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java index 1663096744..a67a3624f8 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -267,13 +267,15 @@ public abstract class BaseAllAppsAdapter ex PrivateProfileManager privateProfileManager = mApps.getPrivateProfileManager(); if (privateProfileManager != null) { // Set the alpha of the private space icon to 0 upon expanding the header so the - // alpha can animate -> 1. + // alpha can animate -> 1. This should only be in effect when doing a + // transitioning between Locked/Unlocked state. boolean isPrivateSpaceItem = privateProfileManager.isPrivateSpaceItem(adapterItem); if (icon.getAlpha() == 0 || icon.getAlpha() == 1) { icon.setAlpha(isPrivateSpaceItem - && (privateProfileManager.getAnimationScrolling() || - privateProfileManager.getAnimate()) + && privateProfileManager.isStateTransitioning() + && (privateProfileManager.isScrolling() || + privateProfileManager.getReadyToAnimate()) && privateProfileManager.getCurrentState() == STATE_ENABLED ? 0 : 1); } diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java index 27340a3678..a620490e4e 100644 --- a/src/com/android/launcher3/allapps/PrivateProfileManager.java +++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java @@ -114,16 +114,21 @@ public class PrivateProfileManager extends UserProfileManager { public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE) { - mAnimationScrolling = false; + mIsScrolling = false; } } }; private Intent mAppInstallerIntent = new Intent(); private PrivateAppsSectionDecorator mPrivateAppsSectionDecorator; private boolean mPrivateSpaceSettingsAvailable; + // Returns if the animation is currently running. private boolean mIsAnimationRunning; - private boolean mAnimate; - private boolean mAnimationScrolling; + // mAnimate denotes if private space is ready to be animated. + private boolean mReadyToAnimate; + // Returns when the recyclerView is currently scrolling. + private boolean mIsScrolling; + // mIsStateTransitioning indicates that private space is transitioning between states. + private boolean mIsStateTransitioning; private Runnable mOnPSHeaderAdded; @Nullable private RelativeLayout mPSHeader; @@ -230,9 +235,11 @@ public class PrivateProfileManager extends UserProfileManager { if (mPSHeader != null) { mPSHeader.setAlpha(1); } - if (transitioningFromLockedToUnlocked(previousState, updatedState)) { + // It's possible that previousState is 0 when reset is first called. + mIsStateTransitioning = previousState != STATE_UNKNOWN && previousState != updatedState; + if (previousState == STATE_DISABLED && updatedState == STATE_ENABLED) { postUnlock(); - } else if (transitioningFromUnlockedToLocked(previousState, updatedState)){ + } else if (previousState == STATE_ENABLED && updatedState == STATE_DISABLED){ executeLock(); } resetPrivateSpaceDecorator(updatedState); @@ -321,7 +328,7 @@ public class PrivateProfileManager extends UserProfileManager { @Override public void setQuietMode(boolean enable) { super.setQuietMode(enable); - mAnimate = true; + mReadyToAnimate = true; } /** @@ -343,7 +350,7 @@ public class PrivateProfileManager extends UserProfileManager { void setAnimationRunning(boolean isAnimationRunning) { if (!isAnimationRunning) { - mAnimate = false; + mReadyToAnimate = false; } mIsAnimationRunning = isAnimationRunning; } @@ -352,14 +359,6 @@ public class PrivateProfileManager extends UserProfileManager { return mIsAnimationRunning; } - private boolean transitioningFromLockedToUnlocked(int previousState, int updatedState) { - return previousState == STATE_DISABLED && updatedState == STATE_ENABLED; - } - - private boolean transitioningFromUnlockedToLocked(int previousState, int updatedState) { - return previousState == STATE_ENABLED && updatedState == STATE_DISABLED; - } - @Override public Predicate getUserMatcher() { return mPrivateProfileMatcher; @@ -386,7 +385,7 @@ public class PrivateProfileManager extends UserProfileManager { } // Set the transition duration for the settings and lock button to animate. ViewGroup settingAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup); - if (mAnimate) { + if (mReadyToAnimate) { enableLayoutTransition(settingAndLockGroup); } else { // Ensure any unwanted animations to not happen. @@ -681,6 +680,7 @@ public class PrivateProfileManager extends UserProfileManager { } }); animatorSet.addListener(forEndCallback(() -> { + mIsStateTransitioning = false; setAnimationRunning(false); getMainRecyclerView().setChildAttachedConsumer(child -> child.setAlpha(1)); mStatsLogManager.logger().sendToInteractionJankMonitor( @@ -712,7 +712,6 @@ public class PrivateProfileManager extends UserProfileManager { animateCollapseAnimation()); } } - animatorSet.setDuration(EXPAND_COLLAPSE_DURATION); animatorSet.start(); } @@ -773,7 +772,7 @@ public class PrivateProfileManager extends UserProfileManager { public void endTransition(LayoutTransition transition, ViewGroup viewGroup, View view, int i) { settingsAndLockGroup.setLayoutTransition(null); - mAnimate = false; + mReadyToAnimate = false; } }); settingsAndLockGroup.setLayoutTransition(settingsAndLockTransition); @@ -873,7 +872,7 @@ public class PrivateProfileManager extends UserProfileManager { /** Starts the smooth scroll with the provided smoothScroller and add idle listener. */ private void startAnimationScroll(AllAppsRecyclerView allAppsRecyclerView, RecyclerView.LayoutManager layoutManager, RecyclerView.SmoothScroller smoothScroller) { - mAnimationScrolling = true; + mIsScrolling = true; layoutManager.startSmoothScroll(smoothScroller); allAppsRecyclerView.removeOnScrollListener(mOnIdleScrollListener); allAppsRecyclerView.addOnScrollListener(mOnIdleScrollListener); @@ -887,12 +886,24 @@ public class PrivateProfileManager extends UserProfileManager { return mAllApps.mAH.get(ActivityAllAppsContainerView.AdapterHolder.MAIN).mRecyclerView; } - boolean getAnimate() { - return mAnimate; + /** Returns if private space is readily available to be animated. */ + boolean getReadyToAnimate() { + return mReadyToAnimate; } - boolean getAnimationScrolling() { - return mAnimationScrolling; + /** Returns when a smooth scroll is happening. */ + boolean isScrolling() { + return mIsScrolling; + } + + /** + * Returns when private space is in the process of transitioning. This is different from + * getAnimate() since mStateTransitioning checks from the time transitioning starts happening + * in reset() as oppose to when private space is animating. This should be used to ensure + * Private Space state during onBind(). + */ + boolean isStateTransitioning() { + return mIsStateTransitioning; } int getPsHeaderHeight() { diff --git a/src/com/android/launcher3/allapps/UserProfileManager.java b/src/com/android/launcher3/allapps/UserProfileManager.java index 3351ee33fc..eb74d20fae 100644 --- a/src/com/android/launcher3/allapps/UserProfileManager.java +++ b/src/com/android/launcher3/allapps/UserProfileManager.java @@ -40,11 +40,13 @@ import java.util.function.Predicate; * {@link PrivateProfileManager} which manages private profile state. */ public abstract class UserProfileManager { + public static final int STATE_UNKNOWN = 0; public static final int STATE_ENABLED = 1; public static final int STATE_DISABLED = 2; public static final int STATE_TRANSITION = 3; @IntDef(value = { + STATE_UNKNOWN, STATE_ENABLED, STATE_DISABLED, STATE_TRANSITION diff --git a/src/com/android/launcher3/dragndrop/LauncherDragController.java b/src/com/android/launcher3/dragndrop/LauncherDragController.java index f3708a2af6..29fc6139f7 100644 --- a/src/com/android/launcher3/dragndrop/LauncherDragController.java +++ b/src/com/android/launcher3/dragndrop/LauncherDragController.java @@ -28,6 +28,7 @@ import android.view.HapticFeedbackConstants; import android.view.View; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DragSource; @@ -36,6 +37,7 @@ import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.accessibility.DragViewStateAnnouncer; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.widget.util.WidgetDragScaleUtils; /** * Drag controller for Launcher activity @@ -43,7 +45,6 @@ import com.android.launcher3.model.data.ItemInfo; public class LauncherDragController extends DragController { private static final boolean PROFILE_DRAWING_DURING_DRAG = false; - private final FlingToDeleteHelper mFlingToDeleteHelper; public LauncherDragController(Launcher launcher) { @@ -92,8 +93,13 @@ public class LauncherDragController extends DragController { && !mOptions.preDragCondition.shouldStartDrag(0); final Resources res = mActivity.getResources(); - final float scaleDps = mIsInPreDrag - ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f; + + final float scalePx; + if (originalView.getViewType() == DraggableView.DRAGGABLE_WIDGET) { + scalePx = mIsInPreDrag ? 0f : getWidgetDragScalePx(drawable, view, dragInfo); + } else { + scalePx = mIsInPreDrag ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f; + } final DragView dragView = mDragObject.dragView = drawable != null ? new LauncherDragView( mActivity, @@ -102,7 +108,7 @@ public class LauncherDragController extends DragController { registrationY, initialDragViewScale, dragViewScaleOnDrop, - scaleDps) + scalePx) : new LauncherDragView( mActivity, view, @@ -112,7 +118,7 @@ public class LauncherDragController extends DragController { registrationY, initialDragViewScale, dragViewScaleOnDrop, - scaleDps); + scalePx); dragView.setItemInfo(dragInfo); mDragObject.dragComplete = false; @@ -157,6 +163,29 @@ public class LauncherDragController extends DragController { return dragView; } + + /** + * Returns the scale in terms of pixels (to be applied on width) to scale the preview + * during drag and drop. + */ + @VisibleForTesting + float getWidgetDragScalePx(@Nullable Drawable drawable, @Nullable View view, + ItemInfo dragInfo) { + float draggedViewWidthPx = 0; + float draggedViewHeightPx = 0; + + if (view != null) { + draggedViewWidthPx = view.getMeasuredWidth(); + draggedViewHeightPx = view.getMeasuredHeight(); + } else if (drawable != null) { + draggedViewWidthPx = drawable.getIntrinsicWidth(); + draggedViewHeightPx = drawable.getIntrinsicHeight(); + } + + return WidgetDragScaleUtils.getWidgetDragScalePx(mActivity, mActivity.getDeviceProfile(), + draggedViewWidthPx, draggedViewHeightPx, dragInfo); + } + @Override protected void exitDrag() { if (!mActivity.isInState(EDIT_MODE)) { diff --git a/src/com/android/launcher3/qsb/QsbContainerView.java b/src/com/android/launcher3/qsb/QsbContainerView.java index 8e53aff1de..d6b41b06a3 100644 --- a/src/com/android/launcher3/qsb/QsbContainerView.java +++ b/src/com/android/launcher3/qsb/QsbContainerView.java @@ -61,7 +61,7 @@ import com.android.launcher3.widget.util.WidgetSizes; */ public class QsbContainerView extends FrameLayout { - public static final String SEARCH_PROVIDER_SETTINGS_KEY = "SEARCH_PROVIDER_PACKAGE_NAME"; + public static final String SEARCH_ENGINE_SETTINGS_KEY = "selected_search_engine"; /** * Returns the package name for user configured search provider or from searchManager @@ -71,8 +71,8 @@ public class QsbContainerView extends FrameLayout { @WorkerThread @Nullable public static String getSearchWidgetPackageName(@NonNull Context context) { - String providerPkg = Settings.Global.getString(context.getContentResolver(), - SEARCH_PROVIDER_SETTINGS_KEY); + String providerPkg = Settings.Secure.getString(context.getContentResolver(), + SEARCH_ENGINE_SETTINGS_KEY); if (providerPkg == null) { SearchManager searchManager = context.getSystemService(SearchManager.class); ComponentName componentName = searchManager.getGlobalSearchActivity(); diff --git a/src/com/android/launcher3/widget/util/WidgetDragScaleUtils.java b/src/com/android/launcher3/widget/util/WidgetDragScaleUtils.java new file mode 100644 index 0000000000..b8e724833e --- /dev/null +++ b/src/com/android/launcher3/widget/util/WidgetDragScaleUtils.java @@ -0,0 +1,68 @@ +/* + * 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.util; + +import static com.android.launcher3.widget.util.WidgetSizes.getWidgetSizePx; + +import android.content.Context; +import android.util.Size; + +import androidx.annotation.Px; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.model.data.ItemInfo; + +/** Utility classes to evaluate widget scale during drag and drops. **/ +public final class WidgetDragScaleUtils { + // Widgets are 5% scaled down relative to their size to have shadow display well inside the + // drop target frame (if its possible to scale it down within visible area under the finger). + private static final float WIDGET_SCALE_DOWN = 0.05f; + + /** + * Returns the scale to be applied to given dragged view to scale it down relative to the + * spring loaded workspace. Applies additional scale down offset to get it a little inside + * the drop target frame. If the relative scale is smaller than minimum size needed to keep the + * view visible under the finger, scale down is performed only until the minimum size. + */ + @Px + public static float getWidgetDragScalePx(Context context, DeviceProfile deviceProfile, + @Px float draggedViewWidthPx, @Px float draggedViewHeightPx, ItemInfo itemInfo) { + int minSize = context.getResources().getDimensionPixelSize( + R.dimen.widget_drag_view_min_scale_down_size); + Size widgetSizesPx = getWidgetSizePx(deviceProfile, itemInfo.spanX, itemInfo.spanY); + + // We add workspace spring load scale, since the widget's drop target is also scaled, so + // the widget size is essentially that smaller. + float desiredWidgetScale = deviceProfile.getWorkspaceSpringLoadScale(context) + - WIDGET_SCALE_DOWN; + float desiredWidgetWidthPx = Math.max(minSize, + (desiredWidgetScale * widgetSizesPx.getWidth())); + float desiredWidgetHeightPx = Math.max(minSize, + desiredWidgetScale * widgetSizesPx.getHeight()); + + final float bitmapAspectRatio = draggedViewWidthPx / draggedViewHeightPx; + final float containerAspectRatio = desiredWidgetWidthPx / desiredWidgetHeightPx; + + // This downscales large views to fit inside drop target frame. Smaller drawable views may + // be up-scaled if they are smaller than the min size; + final float scale = bitmapAspectRatio >= containerAspectRatio ? desiredWidgetWidthPx + / draggedViewWidthPx : desiredWidgetHeightPx / draggedViewHeightPx; + // scale in terms of dp to be applied to the drag shadow during drag and drop + return (draggedViewWidthPx * scale) - draggedViewWidthPx; + } +} diff --git a/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java b/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java index b62dbd1d95..9865516c4c 100644 --- a/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java +++ b/src_no_quickstep/com/android/launcher3/uioverrides/states/AllAppsState.java @@ -52,6 +52,11 @@ public class AllAppsState extends LauncherState { return launcher.getString(R.string.all_apps_button_label); } + @Override + public int getTitle() { + return R.string.all_apps_label; + } + @Override public int getVisibleElements(Launcher launcher) { return ALL_APPS_CONTENT; diff --git a/tests/Android.bp b/tests/Android.bp index a8fba85638..1dcb2a6fe3 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -198,6 +198,9 @@ android_robolectric_test { "androidx.test.uiautomator_uiautomator", "androidx.core_core-animation-testing", "androidx.test.ext.junit", + "androidx.test.espresso.core", + "androidx.test.espresso.contrib", + "androidx.test.espresso.intents", "androidx.test.rules", "uiautomator-helpers", "inline-mockito-robolectric-prebuilt", diff --git a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java index c3b7a2af4f..d20d0fac2c 100644 --- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java +++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java @@ -183,6 +183,9 @@ public final class TestProtocol { public static final String REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED = "unstash-bubble-bar-if-stashed"; + public static final String REQUEST_INJECT_FAKE_TRACKPAD = "inject-fake-trackpad"; + public static final String REQUEST_EJECT_FAKE_TRACKPAD = "eject-fake-trackpad"; + /** Logs {@link Log#d(String, String)} if {@link #sDebugTracing} is true. */ public static void testLogD(String tag, String message) { if (!sDebugTracing) { diff --git a/tests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt similarity index 91% rename from tests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt rename to tests/multivalentTests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt index 7ff544d9f7..5344d5c2c5 100644 --- a/tests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/AbstractFloatingViewHelperTest.kt @@ -25,7 +25,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever /** Test for AbstractFloatingViewHelper */ @@ -60,7 +60,8 @@ class AbstractFloatingViewHelperTest { AbstractFloatingView.TYPE_ALL ) - verifyZeroInteractions(view) + // b/343530737 + verifyNoMoreInteractions(view) verify(folderView).close(true) verify(taskMenuView).close(true) } @@ -73,7 +74,8 @@ class AbstractFloatingViewHelperTest { AbstractFloatingView.TYPE_TASK_MENU ) - verifyZeroInteractions(view) + // b/343530737 + verifyNoMoreInteractions(view) verify(folderView, never()).close(any()) verify(taskMenuView).close(true) } @@ -86,7 +88,8 @@ class AbstractFloatingViewHelperTest { AbstractFloatingView.TYPE_PIN_IME_POPUP ) - verifyZeroInteractions(view) + // b/343530737 + verifyNoMoreInteractions(view) verify(folderView, never()).close(any()) verify(taskMenuView, never()).close(any()) } @@ -99,7 +102,8 @@ class AbstractFloatingViewHelperTest { AbstractFloatingView.TYPE_FOLDER or AbstractFloatingView.TYPE_TASK_MENU ) - verifyZeroInteractions(view) + // b/343530737 + verifyNoMoreInteractions(view) verify(folderView).close(false) verify(taskMenuView).close(false) } diff --git a/tests/src/com/android/launcher3/settings/SettingsActivityTest.java b/tests/multivalentTests/src/com/android/launcher3/settings/SettingsActivityTest.java similarity index 100% rename from tests/src/com/android/launcher3/settings/SettingsActivityTest.java rename to tests/multivalentTests/src/com/android/launcher3/settings/SettingsActivityTest.java diff --git a/tests/src/com/android/launcher3/util/LockedUserStateTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/LockedUserStateTest.kt similarity index 95% rename from tests/src/com/android/launcher3/util/LockedUserStateTest.kt rename to tests/multivalentTests/src/com/android/launcher3/util/LockedUserStateTest.kt index 2c4a54f9d8..2711d7a66d 100644 --- a/tests/src/com/android/launcher3/util/LockedUserStateTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/util/LockedUserStateTest.kt @@ -28,7 +28,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever /** Unit tests for {@link LockedUserState} */ @@ -58,7 +58,8 @@ class LockedUserStateTest { val action: Runnable = mock() val state = LockedUserState(context) state.runOnUserUnlocked(action) - verifyZeroInteractions(action) + // b/343530737 + verifyNoMoreInteractions(action) state.mUserUnlockedReceiver.onReceive(context, Intent(Intent.ACTION_USER_UNLOCKED)) verify(action).run() } diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/util/WidgetDragScaleUtilsTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/util/WidgetDragScaleUtilsTest.kt new file mode 100644 index 0000000000..ec8c9c2bf1 --- /dev/null +++ b/tests/multivalentTests/src/com/android/launcher3/widget/util/WidgetDragScaleUtilsTest.kt @@ -0,0 +1,148 @@ +/* + * 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.util + +import android.content.Context +import android.graphics.Point +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.launcher3.DeviceProfile +import com.android.launcher3.LauncherAppState +import com.android.launcher3.R +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.util.ActivityContextWrapper +import com.google.common.truth.Truth.assertThat +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class WidgetDragScaleUtilsTest { + private lateinit var context: Context + private lateinit var itemInfo: ItemInfo + private lateinit var deviceProfile: DeviceProfile + + @Before + fun setup() { + context = ActivityContextWrapper(ApplicationProvider.getApplicationContext()) + + itemInfo = ItemInfo() + + deviceProfile = + Mockito.spy(LauncherAppState.getIDP(context).getDeviceProfile(context).copy(context)) + + doAnswer { + return@doAnswer 0.8f + } + .whenever(deviceProfile) + .getWorkspaceSpringLoadScale(any(Context::class.java)) + whenever(deviceProfile.cellSize).thenReturn(Point(CELL_SIZE, CELL_SIZE)) + deviceProfile.cellLayoutBorderSpacePx = Point(CELL_SPACING, CELL_SPACING) + deviceProfile.widgetPadding.setEmpty() + } + + @Test + fun getWidgetDragScalePx_largeDraggedView_downScaled() { + itemInfo.spanX = 2 + itemInfo.spanY = 2 + val widgetSize = WidgetSizes.getWidgetSizePx(deviceProfile, itemInfo.spanX, itemInfo.spanY) + // Assume dragged view was a drawable which was larger than widget's size. + val draggedViewWidthPx = widgetSize.width + 0.5f * widgetSize.width + val draggedViewHeightPx = widgetSize.height + 0.5f * widgetSize.height + // Returns negative scale pixels - i.e. downscaled + assertThat( + WidgetDragScaleUtils.getWidgetDragScalePx( + context, + deviceProfile, + draggedViewWidthPx, + draggedViewHeightPx, + itemInfo + ) + ) + .isLessThan(0) + } + + @Test + fun getWidgetDragScalePx_draggedViewSameAsWidgetSize_downScaled() { + itemInfo.spanX = 4 + itemInfo.spanY = 2 + + val widgetSize = WidgetSizes.getWidgetSizePx(deviceProfile, itemInfo.spanX, itemInfo.spanY) + // Assume dragged view was a drawable which was larger than widget's size. + val draggedViewWidthPx = widgetSize.width.toFloat() + val draggedViewHeightPx = widgetSize.height.toFloat() + // Returns negative scale pixels - i.e. downscaled + // Even if dragged view was of same size as widget's drop target, to accommodate the spring + // load scaling of workspace and additionally getting the view inside of drop target frame, + // widget would be downscaled. + assertThat( + WidgetDragScaleUtils.getWidgetDragScalePx( + context, + deviceProfile, + draggedViewWidthPx, + draggedViewHeightPx, + itemInfo + ) + ) + .isLessThan(0) + } + + @Test + fun getWidgetDragScalePx_draggedViewSmallerThanMinSize_scaledSizeIsAtLeastMinSize() { + itemInfo.spanX = 1 + itemInfo.spanY = 1 + val minSizePx = + context.resources.getDimensionPixelSize(R.dimen.widget_drag_view_min_scale_down_size) + + // Assume min size is greater than cell size, so that, we know the upscale of dragged view + // is due to min size enforcement. + assumeTrue(minSizePx > CELL_SIZE) + + val draggedViewWidthPx = minSizePx - 15f + val draggedViewHeightPx = minSizePx - 15f + + // Returns positive scale pixels - i.e. up-scaled + val finalScalePx = + WidgetDragScaleUtils.getWidgetDragScalePx( + context, + deviceProfile, + draggedViewWidthPx, + draggedViewHeightPx, + itemInfo + ) + + val effectiveWidthPx = draggedViewWidthPx + finalScalePx + val scaleFactor = (draggedViewWidthPx + finalScalePx) / draggedViewWidthPx + val effectiveHeightPx = scaleFactor * draggedViewHeightPx + // Both original height and width were smaller than min size, scaling them down below min + // size would have made them not visible under the finger. Here, as expected, widget is + // at least as large as min size. + assertThat(effectiveWidthPx).isAtLeast(minSizePx) + assertThat(effectiveHeightPx).isAtLeast(minSizePx) + } + + companion object { + const val CELL_SIZE = 60 + const val CELL_SPACING = 10 + } +} diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt index 78c61d5c2b..370af0cea5 100644 --- a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt +++ b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt @@ -35,7 +35,7 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.same import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever /** Tests for [AddWorkspaceItemsTask] */ @@ -97,7 +97,8 @@ class AddWorkspaceItemsTaskTest : AbstractWorkspaceModelTest() { val addedItems = testAddItems(nonEmptyScreenIds, itemToAdd) assertThat(addedItems.size).isEqualTo(0) - verifyZeroInteractions(mWorkspaceItemSpaceFinder) + // b/343530737 + verifyNoMoreInteractions(mWorkspaceItemSpaceFinder) } @Test diff --git a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java index 733f1e9a97..b3675a6e0c 100644 --- a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java +++ b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java @@ -38,7 +38,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.app.backup.BackupManager; @@ -243,7 +243,8 @@ public class RestoreDbTaskTest { // Then assertThat(expectedHost.getAppWidgetIds()).isEqualTo(expectedOldIds); assertThat(mPrefs.has(OLD_APP_WIDGET_IDS, APP_WIDGET_IDS)).isFalse(); - verifyZeroInteractions(mMockController); + // b/343530737 + verifyNoMoreInteractions(mMockController); } @Test diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index d85f6303d9..f02a0c2296 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -2318,6 +2318,14 @@ public final class LauncherInstrumentation { getTestInfo(TestProtocol.REQUEST_UNSTASH_BUBBLE_BAR_IF_STASHED); } + public void injectFakeTrackpad() { + getTestInfo(TestProtocol.REQUEST_INJECT_FAKE_TRACKPAD); + } + + public void ejectFakeTrackpad() { + getTestInfo(TestProtocol.REQUEST_EJECT_FAKE_TRACKPAD); + } + /** Blocks the taskbar from automatically stashing based on time. */ public void enableBlockTimeout(boolean enable) { getTestInfo(enable