Snap for 11908474 from 991a40ca92 to 24Q3-release

Change-Id: I38986697fe625519f89a9e792a8ce77aab3d0f43
This commit is contained in:
Android Build Coastguard Worker
2024-05-30 23:22:03 +00:00
29 changed files with 534 additions and 119 deletions
+9 -2
View File
@@ -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}
@@ -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;
@@ -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());
}
@@ -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);
@@ -371,6 +371,9 @@ public final class RecentsActivity extends StatefulActivity<RecentsState> 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
@@ -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<GestureState, AnimatedFloat> mSwipeUpProxyProvider = i -> null;
private AllAppsActionManager mAllAppsActionManager;
private InputManager mInputManager;
private final Set<Integer> 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;
@@ -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();
}
@@ -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()
}
@@ -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() {
@@ -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());
+2
View File
@@ -320,6 +320,8 @@
<dimen name="bg_popup_item_height">52dp</dimen>
<dimen name="bg_popup_item_vertical_padding">12dp</dimen>
<dimen name="pre_drag_view_scale">6dp</dimen>
<!-- Minimum size of the widget dragged view to keep it visible under the finger. -->
<dimen name="widget_drag_view_min_scale_down_size">70dp</dimen>
<!-- an icon with shortcuts must be dragged this far before the container is removed. -->
<dimen name="deep_shortcuts_start_drag_threshold">16dp</dimen>
<!-- Possibly related to b/235886078, icon needs to be scaled up to match expected visual size of 32 dp -->
+1 -5
View File
@@ -1268,11 +1268,7 @@ public class Launcher extends StatefulActivity<LauncherState>
}
// Set screen title for Talkback
if (state == ALL_APPS) {
setTitle(R.string.all_apps_label);
} else {
setTitle(R.string.home_screen);
}
setTitle(state.getTitle());
}
/**
@@ -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<LauncherState> {
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()) {
@@ -267,13 +267,15 @@ public abstract class BaseAllAppsAdapter<T extends Context & ActivityContext> 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);
}
@@ -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<UserHandle> 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() {
@@ -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
@@ -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<Launcher> {
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<Launcher> {
&& !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<Launcher> {
registrationY,
initialDragViewScale,
dragViewScaleOnDrop,
scaleDps)
scalePx)
: new LauncherDragView(
mActivity,
view,
@@ -112,7 +118,7 @@ public class LauncherDragController extends DragController<Launcher> {
registrationY,
initialDragViewScale,
dragViewScaleOnDrop,
scaleDps);
scalePx);
dragView.setItemInfo(dragInfo);
mDragObject.dragComplete = false;
@@ -157,6 +163,29 @@ public class LauncherDragController extends DragController<Launcher> {
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)) {
@@ -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();
@@ -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;
}
}
@@ -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;
+3
View File
@@ -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",
@@ -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) {
@@ -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)
}
@@ -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()
}
@@ -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
}
}
@@ -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
@@ -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
@@ -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