Merge "Add CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS" into tm-qpr-dev
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 02b83fe..951be4e 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -43,7 +43,8 @@
<!-- for rotating surface by arbitrary degree -->
<uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
-
+ <uses-permission android:name="android.permission.READ_HOME_APP_SEARCH_DATA" />
+
<!--
Permissions required for read/write access to the workspace data. These permission name
should not conflict with that defined in other apps, as such an app should embed its package
diff --git a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
index a91ff44..bdac88a 100644
--- a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
+++ b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
@@ -17,6 +17,7 @@
package com.android.launcher3.testing;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.app.Activity;
import android.app.Application;
@@ -248,6 +249,9 @@
return response;
}
+ case TestProtocol.REQUEST_MODEL_QUEUE_CLEARED:
+ return getFromExecutorSync(MODEL_EXECUTOR, Bundle::new);
+
default:
return super.call(method, arg, extras);
}
diff --git a/quickstep/res/drawable/ic_floating_task_button.xml b/quickstep/res/drawable/ic_floating_task_button.xml
deleted file mode 100644
index 63b2fd8..0000000
--- a/quickstep/res/drawable/ic_floating_task_button.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2022 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <group
- android:pivotY="12"
- android:pivotX="12"
- android:scaleX=".75"
- android:scaleY=".75">
- <path
- android:pathData="M17.6258,4.96L19.0358,6.37L7.4058,18.01L5.9958,16.6L17.6258,4.96ZM16.1358,3.62L4.1258,15.63L3.0158,19.83C2.9058,20.45 3.3858,21 3.9958,21C4.0558,21 4.1058,21 4.1658,20.99L8.3658,19.88L20.3758,7.86C20.7758,7.46 20.9958,6.93 20.9958,6.37C20.9958,5.81 20.7758,5.28 20.3758,4.88L19.1058,3.61C18.7158,3.22 18.1858,3 17.6258,3C17.0658,3 16.5358,3.22 16.1358,3.62Z"
- android:fillColor="#636C6F"/>
- <path
- android:pathData="M20.1936,15.3369C20.3748,16.3837 19.9151,17.5414 18.8846,18.7597C19.1546,18.872 19.4576,18.9452 19.7724,18.9867C20.0839,19.0278 20.3683,19.0325 20.5749,19.0266C20.6772,19.0236 20.7578,19.0181 20.8101,19.0138C20.8362,19.0116 20.855,19.0097 20.8657,19.0085L20.8754,19.0074L20.875,19.0075C21.4217,18.9385 21.9214,19.325 21.9918,19.8718C22.0624,20.4195 21.6756,20.9208 21.1279,20.9914L21,19.9996C21.1279,20.9914 21.1265,20.9916 21.1265,20.9916L21.1249,20.9918L21.1211,20.9923L21.1107,20.9935L21.0795,20.997C21.0542,20.9998 21.0199,21.0032 20.9775,21.0067C20.8929,21.0138 20.7753,21.0216 20.6323,21.0257C20.3481,21.0339 19.9533,21.0279 19.5109,20.9695C18.873,20.8854 18.0393,20.6793 17.3106,20.1662C16.9605,20.3559 16.5876,20.4952 16.2299,20.6003C15.5742,20.7927 14.8754,20.8968 14.2534,20.9534C13.6801,21.0055 13.4553,21.0037 13.1015,21.0008C13.0689,21.0005 13.0352,21.0002 13,21H12.8594C12.8214,21.0002 12.785,21.0006 12.7504,21.0009C12.6524,21.0019 12.5683,21.0027 12.5,21H12.0562C12.0277,21.0003 12.0054,21.0006 11.9926,21.001L11.9751,21H9L11,19H11.9795C11.9929,18.9997 12.0064,18.9997 12.0199,19H12.4117C12.4534,18.9996 12.4864,18.9995 12.5,19H12.9675C12.977,18.9999 12.9878,18.9999 13,19C13.0446,19.0003 13.0859,19.0007 13.1249,19.0011C13.4259,19.0038 13.591,19.0054 14.0723,18.9616C14.6201,18.9118 15.1795,18.8242 15.6665,18.6813C15.753,18.6559 15.8346,18.6295 15.9114,18.6022C15.0315,17.2981 14.7125,16.1044 15.015,15.0829C15.4095,13.7511 16.6784,13.2418 17.7026,13.2864C18.7262,13.3309 19.954,13.9529 20.1936,15.3369ZM16.9327,15.6508C16.873,15.8523 16.8651,16.3878 17.4697,17.334C18.2007,16.4284 18.2585,15.8839 18.2229,15.6781C18.1939,15.5108 18.0297,15.3025 17.6157,15.2845C17.2025,15.2665 16.9885,15.4626 16.9327,15.6508Z"
- android:fillColor="#636C6F"
- android:fillType="evenOdd"/>
- </group>
-</vector>
diff --git a/quickstep/res/layout/digital_wellbeing_toast.xml b/quickstep/res/layout/digital_wellbeing_toast.xml
index e493ac9..d5e3670 100644
--- a/quickstep/res/layout/digital_wellbeing_toast.xml
+++ b/quickstep/res/layout/digital_wellbeing_toast.xml
@@ -26,4 +26,5 @@
android:importantForAccessibility="noHideDescendants"
android:textColor="?priv-android:attr/textColorOnAccent"
android:textSize="14sp"
- android:autoSizeTextType="uniform"/>
\ No newline at end of file
+ android:autoSizeTextType="uniform"
+ android:autoSizeMaxTextSize="14sp"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/taskbar_floating_task_button.xml b/quickstep/res/layout/taskbar_floating_task_button.xml
deleted file mode 100644
index b5beded..0000000
--- a/quickstep/res/layout/taskbar_floating_task_button.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2022 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.
--->
-<com.android.launcher3.views.IconButtonView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="@dimen/taskbar_icon_touch_size"
- android:layout_height="@dimen/taskbar_icon_touch_size"
- android:icon="@drawable/ic_floating_task_button"
- />
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 45a2cf8..6e3fd32 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -288,6 +288,13 @@
<dimen name="transient_taskbar_margin">24dp</dimen>
<dimen name="transient_taskbar_shadow_blur">40dp</dimen>
<dimen name="transient_taskbar_key_shadow_distance">10dp</dimen>
+ <dimen name="transient_taskbar_stashed_size">32dp</dimen>
+ <dimen name="transient_taskbar_icon_spacing">10dp</dimen>
+ <!-- Taskbar swipe up thresholds -->
+ <dimen name="taskbar_app_window_threshold">150dp</dimen>
+ <dimen name="taskbar_home_overview_threshold">225dp</dimen>
+ <dimen name="taskbar_catch_up_threshold">300dp</dimen>
+ <dimen name="taskbar_nav_threshold">40dp</dimen>
<!-- Taskbar 3 button spacing -->
<dimen name="taskbar_button_space_inbetween">24dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index 880aa6f..95a94ec 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -55,7 +55,7 @@
* reference to the runner, leaving only the weak ref from the runner.
*/
@TargetApi(Build.VERSION_CODES.P)
-public class LauncherAnimationRunner implements RemoteAnimationRunnerCompat {
+public class LauncherAnimationRunner extends RemoteAnimationRunnerCompat {
private static final RemoteAnimationFactory DEFAULT_FACTORY =
(transit, appTargets, wallpaperTargets, nonAppTargets, result) ->
@@ -99,22 +99,6 @@
}
}
- // Called only in R platform
- @BinderThread
- public void onAnimationStart(RemoteAnimationTarget[] appTargets,
- RemoteAnimationTarget[] wallpaperTargets, Runnable runnable) {
- onAnimationStart(0 /* transit */, appTargets, wallpaperTargets,
- new RemoteAnimationTarget[0], runnable);
- }
-
- // Called only in Q platform
- @BinderThread
- @Deprecated
- public void onAnimationStart(RemoteAnimationTarget[] appTargets, Runnable runnable) {
- onAnimationStart(appTargets, new RemoteAnimationTarget[0], runnable);
- }
-
-
private RemoteAnimationFactory getFactory() {
RemoteAnimationFactory factory = mFactory.get();
return factory != null ? factory : DEFAULT_FACTORY;
@@ -133,7 +117,7 @@
*/
@BinderThread
@Override
- public void onAnimationCancelled() {
+ public void onAnimationCancelled(boolean isKeyguardOccluded) {
postAsyncCallback(mHandler, () -> {
finishExistingAnimation();
getFactory().onAnimationCancelled();
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 938aa5e..1b47939 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -16,11 +16,19 @@
package com.android.launcher3;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_NONE;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
+import static android.window.TransitionFilter.CONTAINER_ORDER_TOP;
import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
@@ -81,6 +89,8 @@
import android.util.Pair;
import android.util.Size;
import android.view.CrossWindowBlurListeners;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationDefinition;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.View;
@@ -90,6 +100,8 @@
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
+import android.window.RemoteTransition;
+import android.window.TransitionFilter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -134,10 +146,7 @@
import com.android.systemui.shared.system.BlurUtils;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.systemui.shared.system.QuickStepContract;
-import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
-import com.android.systemui.shared.system.RemoteAnimationDefinitionCompat;
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
-import com.android.systemui.shared.system.RemoteTransitionCompat;
import com.android.wm.shell.startingsurface.IStartingWindowListener;
import java.util.ArrayList;
@@ -216,7 +225,7 @@
private RemoteAnimationFactory mKeyguardGoingAwayRunner;
private RemoteAnimationFactory mWallpaperOpenTransitionRunner;
- private RemoteTransitionCompat mLauncherOpenTransition;
+ private RemoteTransition mLauncherOpenTransition;
private LauncherBackAnimationController mBackAnimationController;
private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() {
@@ -293,12 +302,10 @@
long statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
- STATUS_BAR_TRANSITION_PRE_DELAY;
- RemoteAnimationAdapterCompat adapterCompat =
- new RemoteAnimationAdapterCompat(runner, duration, statusBarTransitionDelay,
- mLauncher.getIApplicationThread());
ActivityOptions options = ActivityOptions.makeRemoteAnimation(
- adapterCompat.getWrapped(),
- adapterCompat.getRemoteTransition().getTransition());
+ new RemoteAnimationAdapter(runner, duration, statusBarTransitionDelay),
+ new RemoteTransition(runner.toRemoteTransition(),
+ mLauncher.getIApplicationThread()));
return new ActivityOptionsWrapper(options, onEndCallback);
}
@@ -364,13 +371,10 @@
// before our internal listeners.
mLauncher.getStateManager().setCurrentAnimation(anim);
- final int rotationChange = getRotationChange(appTargets);
// Note: the targetBounds are relative to the launcher
int startDelay = getSingleFrameMs(mLauncher);
- Rect windowTargetBounds = getWindowTargetBounds(appTargets, rotationChange);
- Animator windowAnimator = getOpeningWindowAnimators(v, appTargets, wallpaperTargets,
- nonAppTargets, windowTargetBounds, areAllTargetsTranslucent(appTargets),
- rotationChange);
+ Animator windowAnimator = getOpeningWindowAnimators(
+ v, appTargets, wallpaperTargets, nonAppTargets, launcherClosing);
windowAnimator.setStartDelay(startDelay);
anim.play(windowAnimator);
if (launcherClosing) {
@@ -384,17 +388,6 @@
launcherContentAnimator.second.run();
}
});
- } else {
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- mLauncher.addOnResumeCallback(() ->
- ObjectAnimator.ofFloat(mLauncher.getDepthController().stateDepth,
- MULTI_PROPERTY_VALUE,
- mLauncher.getStateManager().getState().getDepth(
- mLauncher)).start());
- }
- });
}
}
@@ -403,23 +396,11 @@
@NonNull LauncherAppWidgetHostView v,
@NonNull RemoteAnimationTarget[] appTargets,
@NonNull RemoteAnimationTarget[] wallpaperTargets,
- @NonNull RemoteAnimationTarget[] nonAppTargets) {
+ @NonNull RemoteAnimationTarget[] nonAppTargets,
+ boolean launcherClosing) {
mLauncher.getStateManager().setCurrentAnimation(anim);
-
- Rect windowTargetBounds = getWindowTargetBounds(appTargets, getRotationChange(appTargets));
- anim.play(getOpeningWindowAnimatorsForWidget(v, appTargets, wallpaperTargets, nonAppTargets,
- windowTargetBounds, areAllTargetsTranslucent(appTargets)));
-
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- mLauncher.addOnResumeCallback(() ->
- ObjectAnimator.ofFloat(mLauncher.getDepthController().stateDepth,
- MULTI_PROPERTY_VALUE,
- mLauncher.getStateManager().getState().getDepth(
- mLauncher)).start());
- }
- });
+ anim.play(getOpeningWindowAnimatorsForWidget(
+ v, appTargets, wallpaperTargets, nonAppTargets, launcherClosing));
}
/**
@@ -657,7 +638,11 @@
RemoteAnimationTarget[] appTargets,
RemoteAnimationTarget[] wallpaperTargets,
RemoteAnimationTarget[] nonAppTargets,
- Rect windowTargetBounds, boolean appTargetsAreTranslucent, int rotationChange) {
+ boolean launcherClosing) {
+ int rotationChange = getRotationChange(appTargets);
+ Rect windowTargetBounds = getWindowTargetBounds(appTargets, rotationChange);
+ boolean appTargetsAreTranslucent = areAllTargetsTranslucent(appTargets);
+
RectF launcherIconBounds = new RectF();
FloatingIconView floatingView = FloatingIconView.getFloatingIconView(mLauncher, v,
!appTargetsAreTranslucent, launcherIconBounds, true /* isOpening */);
@@ -853,7 +838,6 @@
.setShadowRadius(mShadowRadius.value);
} else if (target.mode == MODE_CLOSING) {
if (target.localBounds != null) {
- final Rect localBounds = target.localBounds;
tmpPos.set(target.localBounds.left, target.localBounds.top);
} else {
tmpPos.set(target.position.x, target.position.y);
@@ -898,7 +882,7 @@
// If app targets are translucent, do not animate the background as it causes a visible
// flicker when it resets itself at the end of its animation.
- if (appTargetsAreTranslucent) {
+ if (appTargetsAreTranslucent || !launcherClosing) {
animatorSet.play(appAnimator);
} else {
animatorSet.playTogether(appAnimator, getBackgroundAnimator());
@@ -909,8 +893,10 @@
private Animator getOpeningWindowAnimatorsForWidget(LauncherAppWidgetHostView v,
RemoteAnimationTarget[] appTargets,
RemoteAnimationTarget[] wallpaperTargets,
- RemoteAnimationTarget[] nonAppTargets, Rect windowTargetBounds,
- boolean appTargetsAreTranslucent) {
+ RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing) {
+ Rect windowTargetBounds = getWindowTargetBounds(appTargets, getRotationChange(appTargets));
+ boolean appTargetsAreTranslucent = areAllTargetsTranslucent(appTargets);
+
final RectF widgetBackgroundBounds = new RectF();
final Rect appWindowCrop = new Rect();
final Matrix matrix = new Matrix();
@@ -1037,7 +1023,7 @@
// If app targets are translucent, do not animate the background as it causes a visible
// flicker when it resets itself at the end of its animation.
- if (appTargetsAreTranslucent) {
+ if (appTargetsAreTranslucent || !launcherClosing) {
animatorSet.play(appAnimator);
} else {
animatorSet.playTogether(appAnimator, getBackgroundAnimator());
@@ -1095,28 +1081,26 @@
if (hasControlRemoteAppTransitionPermission()) {
mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */);
- RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat();
+ RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
definition.addRemoteAnimation(WindowManager.TRANSIT_OLD_WALLPAPER_OPEN,
WindowConfiguration.ACTIVITY_TYPE_STANDARD,
- new RemoteAnimationAdapterCompat(
+ new RemoteAnimationAdapter(
new LauncherAnimationRunner(mHandler, mWallpaperOpenRunner,
false /* startAtFrontOfQueue */),
- CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */,
- mLauncher.getIApplicationThread()));
+ CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
if (KEYGUARD_ANIMATION.get()) {
mKeyguardGoingAwayRunner = createWallpaperOpenRunner(true /* fromUnlock */);
definition.addRemoteAnimation(
WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
- new RemoteAnimationAdapterCompat(
+ new RemoteAnimationAdapter(
new LauncherAnimationRunner(
mHandler, mKeyguardGoingAwayRunner,
true /* startAtFrontOfQueue */),
- CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */,
- mLauncher.getIApplicationThread()));
+ CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
}
- mLauncher.registerRemoteAnimations(definition.getWrapped());
+ mLauncher.registerRemoteAnimations(definition);
}
}
@@ -1129,11 +1113,25 @@
}
if (hasControlRemoteAppTransitionPermission()) {
mWallpaperOpenTransitionRunner = createWallpaperOpenRunner(false /* fromUnlock */);
- mLauncherOpenTransition = RemoteAnimationAdapterCompat.buildRemoteTransition(
+ mLauncherOpenTransition = new RemoteTransition(
new LauncherAnimationRunner(mHandler, mWallpaperOpenTransitionRunner,
- false /* startAtFrontOfQueue */), mLauncher.getIApplicationThread());
- mLauncherOpenTransition.addHomeOpenCheck(mLauncher.getComponentName());
- SystemUiProxy.INSTANCE.get(mLauncher).registerRemoteTransition(mLauncherOpenTransition);
+ false /* startAtFrontOfQueue */).toRemoteTransition(),
+ mLauncher.getIApplicationThread());
+
+ TransitionFilter homeCheck = new TransitionFilter();
+ // No need to handle the transition that also dismisses keyguard.
+ homeCheck.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+ homeCheck.mRequirements =
+ new TransitionFilter.Requirement[]{new TransitionFilter.Requirement(),
+ new TransitionFilter.Requirement()};
+ homeCheck.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME;
+ homeCheck.mRequirements[0].mTopActivity = mLauncher.getComponentName();
+ homeCheck.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+ homeCheck.mRequirements[0].mOrder = CONTAINER_ORDER_TOP;
+ homeCheck.mRequirements[1].mActivityType = ACTIVITY_TYPE_STANDARD;
+ homeCheck.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
+ SystemUiProxy.INSTANCE.get(mLauncher)
+ .registerRemoteTransition(mLauncherOpenTransition, homeCheck);
}
if (mBackAnimationController != null) {
mBackAnimationController.registerBackCallbacks(mHandler);
@@ -1717,7 +1715,7 @@
final boolean skipFirstFrame;
if (launchingFromWidget) {
composeWidgetLaunchAnimator(anim, (LauncherAppWidgetHostView) mV, appTargets,
- wallpaperTargets, nonAppTargets);
+ wallpaperTargets, nonAppTargets, launcherClosing);
addCujInstrumentation(
anim, InteractionJankMonitorWrapper.CUJ_APP_LAUNCH_FROM_WIDGET);
skipFirstFrame = true;
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
index 7a483a8..1beabf1 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -18,22 +18,22 @@
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
import android.app.prediction.AppTarget;
-import android.content.ComponentName;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.widget.PendingAddWidgetInfo;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
/** Task to update model as a result of predicted widgets update */
@@ -59,50 +59,43 @@
Set<ComponentKey> widgetsInWorkspace = dataModel.appWidgets.stream().map(
widget -> new ComponentKey(widget.providerName, widget.user)).collect(
Collectors.toSet());
+ Predicate<WidgetItem> notOnWorkspace = w -> !widgetsInWorkspace.contains(w);
Map<PackageUserKey, List<WidgetItem>> allWidgets =
dataModel.widgetsModel.getAllWidgetsWithoutShortcuts();
- FixedContainerItems fixedContainerItems =
- new FixedContainerItems(mPredictorState.containerId);
+ List<WidgetItem> servicePredictedItems = new ArrayList<>();
+ List<WidgetItem> localFilteredWidgets = new ArrayList<>();
- if (FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER.get()) {
- for (AppTarget app : mTargets) {
- PackageUserKey packageUserKey = new PackageUserKey(app.getPackageName(),
- app.getUser());
- if (allWidgets.containsKey(packageUserKey)) {
- List<WidgetItem> notAddedWidgets = allWidgets.get(packageUserKey).stream()
- .filter(item ->
- !widgetsInWorkspace.contains(
- new ComponentKey(item.componentName, item.user)))
- .collect(Collectors.toList());
- if (notAddedWidgets.size() > 0) {
- // Even an apps have more than one widgets, we only include one widget.
- fixedContainerItems.items.add(
- new PendingAddWidgetInfo(
- notAddedWidgets.get(0).widgetInfo,
- CONTAINER_WIDGETS_PREDICTION));
- }
- }
+ for (AppTarget app : mTargets) {
+ PackageUserKey packageUserKey = new PackageUserKey(app.getPackageName(), app.getUser());
+ List<WidgetItem> widgets = allWidgets.get(packageUserKey);
+ if (widgets == null || widgets.isEmpty()) {
+ continue;
}
- } else {
- Map<ComponentKey, WidgetItem> widgetItems =
- allWidgets.values().stream().flatMap(List::stream).distinct()
- .collect(Collectors.toMap(widget -> (ComponentKey) widget,
- widget -> widget));
- for (AppTarget app : mTargets) {
- if (TextUtils.isEmpty(app.getClassName())) {
+ String className = app.getClassName();
+ if (!TextUtils.isEmpty(className)) {
+ WidgetItem item = widgets.stream()
+ .filter(w -> className.equals(w.componentName.getClassName()))
+ .filter(notOnWorkspace)
+ .findFirst()
+ .orElse(null);
+ if (item != null) {
+ servicePredictedItems.add(item);
continue;
}
- ComponentKey targetWidget = new ComponentKey(
- new ComponentName(app.getPackageName(), app.getClassName()), app.getUser());
- if (widgetItems.containsKey(targetWidget)) {
- fixedContainerItems.items.add(
- new PendingAddWidgetInfo(widgetItems.get(
- targetWidget).widgetInfo,
- CONTAINER_WIDGETS_PREDICTION));
- }
}
+ // No widget was added by the service, try local filtering
+ widgets.stream().filter(notOnWorkspace).findFirst()
+ .ifPresent(localFilteredWidgets::add);
}
+ if (servicePredictedItems.isEmpty()) {
+ servicePredictedItems.addAll(localFilteredWidgets);
+ }
+ FixedContainerItems fixedContainerItems =
+ new FixedContainerItems(mPredictorState.containerId);
+ servicePredictedItems.forEach(w -> fixedContainerItems.items.add(
+ new PendingAddWidgetInfo(w.widgetInfo, CONTAINER_WIDGETS_PREDICTION)));
+
dataModel.extraItems.put(mPredictorState.containerId, fixedContainerItems);
bindExtraContainerItems(fixedContainerItems);
diff --git a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
index f1e6747..08ed60d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/FallbackTaskbarUIController.java
@@ -21,6 +21,7 @@
import android.animation.Animator;
+import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StateManager;
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.fallback.RecentsState;
@@ -70,12 +71,14 @@
* Currently this animation just force stashes the taskbar in Overview.
*/
public Animator createAnimToRecentsState(RecentsState toState, long duration) {
- boolean forceStashed = toState.hasOverviewActions();
+ boolean useStashedLauncherState = toState.hasOverviewActions();
+ boolean stashedLauncherState =
+ useStashedLauncherState && !FeatureFlags.ENABLE_TASKBAR_IN_OVERVIEW.get();
TaskbarStashController controller = mControllers.taskbarStashController;
// Set both FLAG_IN_STASHED_LAUNCHER_STATE and FLAG_IN_APP to ensure the state is respected.
// For all other states, just use the current stashed-in-app setting (e.g. if long clicked).
- controller.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, forceStashed);
- controller.updateStateForFlag(FLAG_IN_APP, !forceStashed);
+ controller.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, stashedLauncherState);
+ controller.updateStateForFlag(FLAG_IN_APP, !useStashedLauncherState);
return controller.applyStateWithoutStart(duration);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/FloatingTaskIntentResolver.java b/quickstep/src/com/android/launcher3/taskbar/FloatingTaskIntentResolver.java
deleted file mode 100644
index 5f4d239..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/FloatingTaskIntentResolver.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2022 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.taskbar;
-
-import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
-
-import android.annotation.Nullable;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.launcher3.R;
-
-// TODO: This would be replaced by the thing that has the role and provides the intent.
-/**
- * Helper to determine what intent should be used to display in a floating window, if one
- * exists.
- */
-public class FloatingTaskIntentResolver {
- private static final String TAG = FloatingTaskIntentResolver.class.getSimpleName();
-
- @Nullable
- /** Gets an intent for a floating task, if one exists. */
- public static Intent getIntent(Context context) {
- PackageManager pm = context.getPackageManager();
- String pkg = context.getString(R.string.floating_task_package);
- String action = context.getString(R.string.floating_task_action);
- if (TextUtils.isEmpty(pkg) || TextUtils.isEmpty(action)) {
- Log.d(TAG, "intent could not be found, pkg= " + pkg + " action= " + action);
- return null;
- }
- Intent intent = createIntent(pm, null, pkg, action);
- if (intent != null) {
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- return intent;
- }
- Log.d(TAG, "No valid intent found!");
- return null;
- }
-
- @Nullable
- private static Intent createIntent(PackageManager pm, @Nullable String activityName,
- String packageName, String action) {
- if (TextUtils.isEmpty(activityName)) {
- activityName = queryActivityForAction(pm, packageName, action);
- }
- if (TextUtils.isEmpty(activityName)) {
- Log.d(TAG, "Activity name is empty even after action search: " + action);
- return null;
- }
- ComponentName component = new ComponentName(packageName, activityName);
- Intent intent = new Intent(action).setComponent(component).setPackage(packageName);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- Log.d(TAG, "createIntent returning: " + intent);
- return intent;
- }
-
- @Nullable
- private static String queryActivityForAction(PackageManager pm, String packageName,
- String action) {
- Intent intent = new Intent(action).setPackage(packageName);
- ResolveInfo resolveInfo = pm.resolveActivity(intent, MATCH_DEFAULT_ONLY);
- if (resolveInfo == null || resolveInfo.activityInfo == null) {
- Log.d(TAG, "queryActivityForAction: + " + resolveInfo);
- return null;
- }
- ActivityInfo info = resolveInfo.activityInfo;
- if (!info.exported) {
- Log.d(TAG, "queryActivityForAction: + " + info + " not exported");
- return null;
- }
- if (!info.enabled) {
- Log.d(TAG, "queryActivityForAction: + " + info + " not enabled");
- return null;
- }
- return resolveInfo.activityInfo.name;
- }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index c9e42b7..174f97f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -26,7 +26,6 @@
import android.os.RemoteException;
import android.util.Log;
import android.util.SparseArray;
-import android.view.MotionEvent;
import android.view.TaskTransitionSpec;
import android.view.WindowManagerGlobal;
@@ -193,16 +192,15 @@
*/
public Animator createAnimToLauncher(@NonNull LauncherState toState,
@NonNull RecentsAnimationCallbacks callbacks, long duration) {
- return mTaskbarLauncherStateController.createAnimToLauncher(toState, callbacks, duration);
- }
+ AnimatorSet set = new AnimatorSet();
+ Animator taskbarState = mTaskbarLauncherStateController
+ .createAnimToLauncher(toState, callbacks, duration);
+ long halfDuration = Math.round(duration * 0.5f);
+ Animator translation =
+ mControllers.taskbarTranslationController.createAnimToLauncher(halfDuration);
- /**
- * @param ev MotionEvent in screen coordinates.
- * @return Whether any Taskbar item could handle the given MotionEvent if given the chance.
- */
- public boolean isEventOverAnyTaskbarItem(MotionEvent ev) {
- return mControllers.taskbarViewController.isEventOverAnyItem(ev)
- || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev);
+ set.playTogether(taskbarState, translation);
+ return set;
}
public boolean isDraggingItem() {
@@ -356,6 +354,11 @@
}
@Override
+ public boolean isIconAlignedWithHotseat() {
+ return mTaskbarLauncherStateController.isIconAlignedWithHotseat();
+ }
+
+ @Override
public void dumpLogs(String prefix, PrintWriter pw) {
super.dumpLogs(prefix, pw);
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index 12dbcb3..45d5739 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -30,6 +30,7 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.RevealOutlineAnimation;
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
@@ -66,6 +67,7 @@
// Initialized in init.
private TaskbarControllers mControllers;
+ private int mTaskbarSize;
// The bounds we want to clip to in the settled state when showing the stashed handle.
private final Rect mStashedHandleBounds = new Rect();
@@ -96,15 +98,18 @@
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
Resources resources = mActivity.getResources();
if (isPhoneGestureNavMode(mActivity.getDeviceProfile())) {
- mStashedHandleView.getLayoutParams().height =
- resources.getDimensionPixelSize(R.dimen.taskbar_size);
+ mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_size);
mStashedHandleWidth =
resources.getDimensionPixelSize(R.dimen.taskbar_stashed_small_screen);
} else {
- mStashedHandleView.getLayoutParams().height = deviceProfile.taskbarSize;
+ mTaskbarSize = deviceProfile.taskbarSize;
mStashedHandleWidth = resources
.getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width);
}
+ int taskbarBottomMargin = DisplayController.isTransientTaskbar(mActivity)
+ ? resources.getDimensionPixelSize(R.dimen.transient_taskbar_margin)
+ : 0;
+ mStashedHandleView.getLayoutParams().height = mTaskbarSize + taskbarBottomMargin;
mTaskbarStashedHandleAlpha.get(ALPHA_INDEX_STASHED).setValue(
isPhoneGestureNavMode(deviceProfile) ? 1 : 0);
@@ -181,9 +186,17 @@
* morphs into the size of where the taskbar icons will be.
*/
public Animator createRevealAnimToIsStashed(boolean isStashed) {
+ Rect visualBounds = new Rect(mControllers.taskbarViewController.getIconLayoutBounds());
+
+ if (DisplayController.isTransientTaskbar(mActivity)) {
+ // Account for the full visual height of the transient taskbar.
+ int heightDiff = (mTaskbarSize - visualBounds.height()) / 2;
+ visualBounds.top -= heightDiff;
+ visualBounds.bottom += heightDiff;
+ }
+
final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
- mStashedHandleRadius, mStashedHandleRadius,
- mControllers.taskbarViewController.getIconLayoutBounds(), mStashedHandleBounds);
+ mStashedHandleRadius, mStashedHandleRadius, visualBounds, mStashedHandleBounds);
boolean isReversed = !isStashed;
boolean changingDirection = mWasLastRevealAnimReversed != isReversed;
@@ -220,6 +233,13 @@
}
/**
+ * Sets the translation of the stashed handle during the swipe up gesture.
+ */
+ protected void setTranslationYForSwipe(float transY) {
+ mStashedHandleView.setTranslationY(transY);
+ }
+
+ /**
* Should be called when the home button is disabled, so we can hide this handle as well.
*/
public void setIsHomeButtonDisabled(boolean homeDisabled) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 593605f..3c9e96f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -24,6 +24,8 @@
import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
+import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING;
+import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_FULLSCREEN;
import static com.android.launcher3.taskbar.TaskbarManager.FLAG_HIDE_NAVBAR_WINDOW;
import static com.android.launcher3.testing.shared.ResourceUtils.getBoolByName;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
@@ -75,6 +77,8 @@
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.AutohideSuspendFlag;
+import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
import com.android.launcher3.testing.TestLogging;
@@ -220,6 +224,7 @@
new TaskbarAllAppsController(),
new TaskbarInsetsController(this),
new VoiceInteractionWindowController(this),
+ new TaskbarTranslationController(this),
isDesktopMode
? new DesktopTaskbarRecentAppsController(this)
: TaskbarRecentAppsController.DEFAULT);
@@ -297,13 +302,19 @@
// Taskbar is on the logical bottom of the screen
boolean isVerticalBarLayout = TaskbarManager.isPhoneMode(deviceProfile) &&
deviceProfile.isLandscape;
+
+ int windowFlags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_SLIPPERY
+ | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
+ if (DisplayController.isTransientTaskbar(this)) {
+ windowFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+ }
WindowManager.LayoutParams windowLayoutParams = new WindowManager.LayoutParams(
isVerticalBarLayout ? mLastRequestedNonFullscreenHeight : MATCH_PARENT,
isVerticalBarLayout ? MATCH_PARENT : mLastRequestedNonFullscreenHeight,
type,
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_SLIPPERY
- | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+ windowFlags,
PixelFormat.TRANSLUCENT);
windowLayoutParams.setTitle(WINDOW_TITLE);
windowLayoutParams.packageName = getPackageName();
@@ -467,7 +478,7 @@
@Override
public void onDragEnd() {
- maybeSetTaskbarWindowNotFullscreen();
+ onDragEndOrViewRemoved();
}
@Override
@@ -572,24 +583,33 @@
}
/**
+ * Called to update a {@link AutohideSuspendFlag} with a new value.
+ */
+ public void setAutohideSuspendFlag(@AutohideSuspendFlag int flag, boolean newValue) {
+ mControllers.taskbarAutohideSuspendController.updateFlag(flag, newValue);
+ }
+
+ /**
* Updates the TaskbarContainer to MATCH_PARENT vs original Taskbar size.
*/
public void setTaskbarWindowFullscreen(boolean fullscreen) {
- mControllers.taskbarAutohideSuspendController.updateFlag(
- TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_FULLSCREEN, fullscreen);
+ setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_FULLSCREEN, fullscreen);
mIsFullscreen = fullscreen;
setTaskbarWindowHeight(fullscreen ? MATCH_PARENT : mLastRequestedNonFullscreenHeight);
}
/**
- * Reverts Taskbar window to its original size, if all floating views are closed and there is
- * no system drag operation in progress.
+ * Called when drag ends or when a view is removed from the DragLayer.
*/
- void maybeSetTaskbarWindowNotFullscreen() {
- if (AbstractFloatingView.getAnyView(this, TYPE_ALL) == null
- && !mControllers.taskbarDragController.isSystemDragInProgress()) {
+ void onDragEndOrViewRemoved() {
+ boolean isDragInProgress = mControllers.taskbarDragController.isSystemDragInProgress();
+
+ if (!isDragInProgress && !AbstractFloatingView.hasOpenView(this, TYPE_ALL)) {
+ // Reverts Taskbar window to its original size
setTaskbarWindowFullscreen(false);
}
+
+ setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_DRAGGING, isDragInProgress);
}
public boolean isTaskbarWindowFullscreen() {
@@ -703,6 +723,7 @@
Task task = (Task) tag;
ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
ActivityOptions.makeBasic());
+ mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
} else if (tag instanceof FolderInfo) {
FolderIcon folderIcon = (FolderIcon) view;
Folder folder = folderIcon.getFolder();
@@ -762,6 +783,7 @@
}
mControllers.uiController.onTaskbarIconLaunched(info);
+ mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
} catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT)
.show();
@@ -771,6 +793,7 @@
} else if (tag instanceof AppInfo) {
startItemInfoActivity((AppInfo) tag);
mControllers.uiController.onTaskbarIconLaunched((AppInfo) tag);
+ mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
} else {
Log.e(TAG, "Unknown type clicked: " + tag);
}
@@ -806,6 +829,34 @@
}
/**
+ * Called when we want to unstash taskbar when user performs swipes up gesture.
+ */
+ public void onSwipeToUnstashTaskbar() {
+ mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false);
+ }
+
+ /**
+ * Called to start the taskbar translation spring to its settled translation (0).
+ */
+ public void startTranslationSpring() {
+ mControllers.taskbarTranslationController.startSpring();
+ }
+
+ /**
+ * Returns a callback to help monitor the swipe gesture.
+ */
+ public TransitionCallback getTranslationCallbacks() {
+ return mControllers.taskbarTranslationController.getTransitionCallback();
+ }
+
+ /**
+ * Called when a transient Autohide flag suspend status changes.
+ */
+ public void onTransientAutohideSuspendFlagChanged(boolean isSuspended) {
+ mControllers.taskbarStashController.updateTaskbarTimeout(isSuspended);
+ }
+
+ /**
* Called when we detect a motion down or up/cancel in the nav region while stashed.
* @param animateForward Whether to animate towards the unstashed hint state or back to stashed.
*/
@@ -928,4 +979,9 @@
mControllers.dumpLogs(prefix + "\t", pw);
mDeviceProfile.dump(this, prefix, pw);
}
+
+ @VisibleForTesting
+ public int getTaskbarAllAppsTopPadding() {
+ return mControllers.taskbarAllAppsController.getTaskbarAllAppsTopPadding();
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
index 3cf9c99..4350e9c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java
@@ -33,21 +33,28 @@
public class TaskbarAutohideSuspendController implements
TaskbarControllers.LoggableTaskbarController {
+ // Taskbar window is fullscreen.
public static final int FLAG_AUTOHIDE_SUSPEND_FULLSCREEN = 1 << 0;
+ // User is dragging item.
public static final int FLAG_AUTOHIDE_SUSPEND_DRAGGING = 1 << 1;
+ // User has touched down but has not lifted finger.
+ public static final int FLAG_AUTOHIDE_SUSPEND_TOUCHING = 1 << 2;
@IntDef(flag = true, value = {
FLAG_AUTOHIDE_SUSPEND_FULLSCREEN,
FLAG_AUTOHIDE_SUSPEND_DRAGGING,
+ FLAG_AUTOHIDE_SUSPEND_TOUCHING,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AutohideSuspendFlag {}
+ private final TaskbarActivityContext mActivity;
private final SystemUiProxy mSystemUiProxy;
private @AutohideSuspendFlag int mAutohideSuspendFlags = 0;
public TaskbarAutohideSuspendController(TaskbarActivityContext activity) {
+ mActivity = activity;
mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity);
}
@@ -59,12 +66,27 @@
* Adds or removes the given flag, then notifies system UI proxy whether to suspend auto-hide.
*/
public void updateFlag(@AutohideSuspendFlag int flag, boolean enabled) {
+ int flagsBefore = mAutohideSuspendFlags;
if (enabled) {
mAutohideSuspendFlags |= flag;
} else {
mAutohideSuspendFlags &= ~flag;
}
- mSystemUiProxy.notifyTaskbarAutohideSuspend(mAutohideSuspendFlags != 0);
+ if (flagsBefore == mAutohideSuspendFlags) {
+ // Nothing has changed, no need to notify.
+ return;
+ }
+
+ boolean isSuspended = isSuspended();
+ mSystemUiProxy.notifyTaskbarAutohideSuspend(isSuspended);
+ mActivity.onTransientAutohideSuspendFlagChanged(isSuspended);
+ }
+
+ /**
+ * Returns true iff taskbar autohide is currently suspended.
+ */
+ public boolean isSuspended() {
+ return mAutohideSuspendFlags != 0;
}
@Override
@@ -79,6 +101,7 @@
appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_FULLSCREEN,
"FLAG_AUTOHIDE_SUSPEND_FULLSCREEN");
appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_DRAGGING, "FLAG_AUTOHIDE_SUSPEND_DRAGGING");
+ appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_TOUCHING, "FLAG_AUTOHIDE_SUSPEND_TOUCHING");
return str.toString();
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index abd467d..ff7e8e9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -34,6 +34,7 @@
val paint: Paint = Paint()
var backgroundHeight = context.deviceProfile.taskbarSize.toFloat()
+ var translationYForSwipe = 0f
private var maxBackgroundHeight = context.deviceProfile.taskbarSize.toFloat()
private val transientBackgroundBounds = context.transientTaskbarBounds
@@ -44,8 +45,12 @@
private var keyShadowDistance = 0f
private var bottomMargin = 0
- private val leftCornerRadius = context.leftCornerRadius.toFloat()
- private val rightCornerRadius = context.rightCornerRadius.toFloat()
+ private val fullLeftCornerRadius = context.leftCornerRadius.toFloat()
+ private val fullRightCornerRadius = context.rightCornerRadius.toFloat()
+ private var leftCornerRadius = fullLeftCornerRadius
+ private var rightCornerRadius = fullRightCornerRadius
+ private val square: Path = Path()
+ private val circle: Path = Path()
private val invertedLeftCornerPath: Path = Path()
private val invertedRightCornerPath: Path = Path()
@@ -63,13 +68,29 @@
keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance)
}
+ setCornerRoundness(DEFAULT_ROUNDNESS)
+ }
+
+ /**
+ * Sets the roundness of the round corner above Taskbar. No effect on transient Taskkbar.
+ * @param cornerRoundness 0 has no round corner, 1 has complete round corner.
+ */
+ fun setCornerRoundness(cornerRoundness: Float) {
+ if (isTransientTaskbar && !transientBackgroundBounds.isEmpty) {
+ return
+ }
+
+ leftCornerRadius = fullLeftCornerRadius * cornerRoundness
+ rightCornerRadius = fullRightCornerRadius * cornerRoundness
+
// Create the paths for the inverted rounded corners above the taskbar. Start with a filled
// square, and then subtract out a circle from the appropriate corner.
- val square = Path()
+ square.reset()
square.addRect(0f, 0f, leftCornerRadius, leftCornerRadius, Path.Direction.CW)
- val circle = Path()
+ circle.reset()
circle.addCircle(leftCornerRadius, 0f, leftCornerRadius, Path.Direction.CW)
invertedLeftCornerPath.op(square, circle, Path.Op.DIFFERENCE)
+
square.reset()
square.addRect(0f, 0f, rightCornerRadius, rightCornerRadius, Path.Direction.CW)
circle.reset()
@@ -94,11 +115,13 @@
canvas.translate(canvas.width - rightCornerRadius, -rightCornerRadius)
canvas.drawPath(invertedRightCornerPath, paint)
} else {
+ // Approximates the stash/unstash animation to transform the background.
val scaleFactor = backgroundHeight / maxBackgroundHeight
val width = transientBackgroundBounds.width()
val widthScale = mapToRange(scaleFactor, 0f, 1f, 0.4f, 1f, Interpolators.LINEAR)
val newWidth = widthScale * width
val delta = width - newWidth
+ canvas.translate(0f, bottomMargin * ((1f - scaleFactor) / 2f))
// Draw shadow.
val shadowAlpha = mapToRange(paint.alpha.toFloat(), 0f, 255f, 0f, 25f,
@@ -112,13 +135,17 @@
canvas.drawRoundRect(
transientBackgroundBounds.left + (delta / 2f),
- 0f,
+ translationYForSwipe,
transientBackgroundBounds.right - (delta / 2f),
- backgroundHeight,
+ backgroundHeight + translationYForSwipe,
radius, radius, paint
)
}
canvas.restore()
}
+
+ companion object {
+ const val DEFAULT_ROUNDNESS = 1f
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 9c2d21e..0328df9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -23,6 +23,7 @@
import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
+import com.android.quickstep.AnimatedFloat;
import com.android.systemui.shared.rotation.RotationButtonController;
import java.io.PrintWriter;
@@ -55,9 +56,11 @@
public final TaskbarInsetsController taskbarInsetsController;
public final VoiceInteractionWindowController voiceInteractionWindowController;
public final TaskbarRecentAppsController taskbarRecentAppsController;
+ public final TaskbarTranslationController taskbarTranslationController;
public final TaskbarOverlayController taskbarOverlayController;
@Nullable private LoggableTaskbarController[] mControllersToLog = null;
+ @Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null;
/** Do not store this controller, as it may change at runtime. */
@NonNull public TaskbarUIController uiController = TaskbarUIController.DEFAULT;
@@ -67,6 +70,9 @@
@Nullable private TaskbarSharedState mSharedState = null;
+ // Roundness property for round corner above taskbar .
+ private final AnimatedFloat mCornerRoundness = new AnimatedFloat(this::updateCornerRoundness);
+
public TaskbarControllers(TaskbarActivityContext taskbarActivityContext,
TaskbarDragController taskbarDragController,
TaskbarNavButtonController navButtonController,
@@ -87,6 +93,7 @@
TaskbarAllAppsController taskbarAllAppsController,
TaskbarInsetsController taskbarInsetsController,
VoiceInteractionWindowController voiceInteractionWindowController,
+ TaskbarTranslationController taskbarTranslationController,
TaskbarRecentAppsController taskbarRecentAppsController) {
this.taskbarActivityContext = taskbarActivityContext;
this.taskbarDragController = taskbarDragController;
@@ -108,6 +115,7 @@
this.taskbarAllAppsController = taskbarAllAppsController;
this.taskbarInsetsController = taskbarInsetsController;
this.voiceInteractionWindowController = voiceInteractionWindowController;
+ this.taskbarTranslationController = taskbarTranslationController;
this.taskbarRecentAppsController = taskbarRecentAppsController;
}
@@ -139,6 +147,7 @@
taskbarInsetsController.init(this);
voiceInteractionWindowController.init(this);
taskbarRecentAppsController.init(this);
+ taskbarTranslationController.init(this);
mControllersToLog = new LoggableTaskbarController[] {
taskbarDragController, navButtonController, navbarButtonsViewController,
@@ -146,8 +155,13 @@
taskbarUnfoldAnimationController, taskbarKeyguardController,
stashedHandleViewController, taskbarStashController, taskbarEduController,
taskbarAutohideSuspendController, taskbarPopupController, taskbarInsetsController,
+ voiceInteractionWindowController, taskbarTranslationController
+ };
+ mBackgroundRendererControllers = new BackgroundRendererController[] {
+ taskbarDragLayerController, taskbarScrimViewController,
voiceInteractionWindowController
};
+ mCornerRoundness.updateValue(TaskbarBackgroundRenderer.DEFAULT_ROUNDNESS);
mAreAllControllersInitialized = true;
for (Runnable postInitCallback : mPostInitCallbacks) {
@@ -164,6 +178,7 @@
public void onConfigurationChanged(@Config int configChanges) {
navbarButtonsViewController.onConfigurationChanged(configChanges);
+ taskbarDragLayerController.onConfigurationChanged();
}
/**
@@ -191,6 +206,7 @@
taskbarRecentAppsController.onDestroy();
mControllersToLog = null;
+ mBackgroundRendererControllers = null;
}
/**
@@ -224,6 +240,23 @@
rotationButtonController.dumpLogs(prefix + "\t", pw);
}
+ /**
+ * Returns a float property that animates roundness of the round corner above Taskbar.
+ */
+ public AnimatedFloat getTaskbarCornerRoundness() {
+ return mCornerRoundness;
+ }
+
+ private void updateCornerRoundness() {
+ if (mBackgroundRendererControllers == null) {
+ return;
+ }
+
+ for (BackgroundRendererController controller : mBackgroundRendererControllers) {
+ controller.setCornerRoundness(mCornerRoundness.value);
+ }
+ }
+
@VisibleForTesting
TaskbarActivityContext getTaskbarActivityContext() {
// Used to mock
@@ -233,4 +266,12 @@
protected interface LoggableTaskbarController {
void dumpLogs(String prefix, PrintWriter pw);
}
+
+ protected interface BackgroundRendererController {
+ /**
+ * Sets the roundness of the round corner above Taskbar.
+ * @param cornerRoundness 0 has no round corner, 1 has complete round corner.
+ */
+ void setCornerRoundness(float cornerRoundness);
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 9a1e064..d7bb16e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -18,10 +18,12 @@
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
+import android.annotation.NonNull;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Intent;
@@ -49,7 +51,6 @@
import com.android.launcher3.DropTarget;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
import com.android.launcher3.accessibility.DragViewStateAnnouncer;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.config.FeatureFlags;
@@ -70,6 +71,7 @@
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.quickstep.util.LogUtils;
+import com.android.quickstep.util.MultiValueUpdateListener;
import com.android.systemui.shared.recents.model.Task;
import java.io.PrintWriter;
@@ -83,7 +85,8 @@
public class TaskbarDragController extends DragController<BaseTaskbarContext> implements
TaskbarControllers.LoggableTaskbarController {
- private static boolean DEBUG_DRAG_SHADOW_SURFACE = false;
+ private static final boolean DEBUG_DRAG_SHADOW_SURFACE = false;
+ private static final int ANIM_DURATION_RETURN_ICON_TO_TASKBAR = 300;
private final int mDragIconSize;
private final int[] mTempXY = new int[2];
@@ -99,6 +102,8 @@
// Animation for the drag shadow back into position after an unsuccessful drag
private ValueAnimator mReturnAnimator;
+ private boolean mDisallowGlobalDrag;
+ private boolean mDisallowLongClick;
public TaskbarDragController(BaseTaskbarContext activity) {
super(activity);
@@ -110,6 +115,14 @@
mControllers = controllers;
}
+ public void setDisallowGlobalDrag(boolean disallowGlobalDrag) {
+ mDisallowGlobalDrag = disallowGlobalDrag;
+ }
+
+ public void setDisallowLongClick(boolean disallowLongClick) {
+ mDisallowLongClick = disallowLongClick;
+ }
+
/**
* Attempts to start a system drag and drop operation for the given View, using its tag to
* generate the ClipDescription and Intent.
@@ -131,7 +144,7 @@
View view,
@Nullable DragPreviewProvider dragPreviewProvider,
@Nullable Point iconShift) {
- if (!(view instanceof BubbleTextView)) {
+ if (!(view instanceof BubbleTextView) || mDisallowLongClick) {
return false;
}
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onTaskbarItemLongClick");
@@ -293,6 +306,7 @@
}
private void startSystemDrag(BubbleTextView btv) {
+ if (mDisallowGlobalDrag) return;
View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(btv) {
@Override
@@ -422,6 +436,45 @@
}
@Override
+ protected void endDrag() {
+ if (mDisallowGlobalDrag) {
+ // We need to explicitly set deferDragViewCleanupPostAnimation to true here so the
+ // super call doesn't remove it from the drag layer before the animation completes.
+ // This variable gets set in to false in super.dispatchDropComplete() because it
+ // (rightfully so, perhaps) thinks this drag operation has failed, and does its own
+ // internal cleanup.
+ // Another way to approach this would be to make all of overview a drop target and
+ // accept the drop as successful and then run the setupReturnDragAnimator to simulate
+ // drop failure to the user
+ mDragObject.deferDragViewCleanupPostAnimation = true;
+
+ float fromX = mDragObject.x - mDragObject.xOffset;
+ float fromY = mDragObject.y - mDragObject.yOffset;
+ DragView dragView = mDragObject.dragView;
+ setupReturnDragAnimator(fromX, fromY, (View) mDragObject.originalView,
+ (x, y, scale, alpha) -> {
+ dragView.setTranslationX(x);
+ dragView.setTranslationY(y);
+ dragView.setScaleX(scale);
+ dragView.setScaleY(scale);
+ dragView.setAlpha(alpha);
+ });
+ mReturnAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ callOnDragEnd();
+ dragView.remove();
+ dragView.clearAnimation();
+ mReturnAnimator = null;
+
+ }
+ });
+ mReturnAnimator.start();
+ }
+ super.endDrag();
+ }
+
+ @Override
protected void callOnDragEnd() {
super.callOnDragEnd();
maybeOnDragEnd();
@@ -432,56 +485,20 @@
SurfaceControl dragSurface = dragEvent.getDragSurface();
// For top level icons, the target is the icon itself
- View target = btv;
- Object tag = btv.getTag();
- if (tag instanceof ItemInfo) {
- ItemInfo item = (ItemInfo) tag;
- TaskbarViewController taskbarViewController = mControllers.taskbarViewController;
- if (item.container == CONTAINER_ALL_APPS || item.container == CONTAINER_PREDICTION) {
- // Since all apps closes when the drag starts, target the all apps button instead.
- target = taskbarViewController.getAllAppsButtonView();
- } else if (item.container >= 0) {
- // Since folders close when the drag starts, target the folder icon instead.
- Predicate<ItemInfo> matcher = ItemInfoMatcher.forFolderMatch(
- ItemInfoMatcher.ofItemIds(IntSet.wrap(item.id)));
- target = taskbarViewController.getFirstIconMatch(matcher);
- } else if (item.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
- // Find first icon with same package/user as the deep shortcut.
- Predicate<ItemInfo> packageUserMatcher = ItemInfoMatcher.ofPackages(
- Collections.singleton(item.getTargetPackage()), item.user);
- target = taskbarViewController.getFirstIconMatch(packageUserMatcher);
- }
- }
-
- // Finish any pending return animation before starting a new drag
- if (mReturnAnimator != null) {
- mReturnAnimator.end();
- }
+ View target = findTaskbarTargetForIconView(btv);
float fromX = dragEvent.getX() - dragEvent.getOffsetX();
float fromY = dragEvent.getY() - dragEvent.getOffsetY();
- int[] toPosition = target.getLocationOnScreen();
- float toScale = (float) target.getWidth() / mDragIconSize;
- float toAlpha = (target == btv) ? 1f : 0f;
final ViewRootImpl viewRoot = target.getViewRootImpl();
SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
- mReturnAnimator = ValueAnimator.ofFloat(0f, 1f);
- mReturnAnimator.setDuration(300);
- mReturnAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
- mReturnAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- float t = animation.getAnimatedFraction();
- float accelT = Interpolators.ACCEL_2.getInterpolation(t);
- float scale = 1f - t * (1f - toScale);
- float alpha = 1f - accelT * (1f - toAlpha);
- tx.setPosition(dragSurface, Utilities.mapRange(t, fromX, toPosition[0]),
- Utilities.mapRange(t, fromY, toPosition[1]));
- tx.setScale(dragSurface, scale, scale);
- tx.setAlpha(dragSurface, alpha);
- tx.apply();
- }
- });
+ setupReturnDragAnimator(fromX, fromY, btv,
+ (x, y, scale, alpha) -> {
+ tx.setPosition(dragSurface, x, y);
+ tx.setScale(dragSurface, scale, scale);
+ tx.setAlpha(dragSurface, alpha);
+ tx.apply();
+ });
+
mReturnAnimator.addListener(new AnimatorListenerAdapter() {
private boolean mCanceled = false;
@@ -517,6 +534,63 @@
mReturnAnimator.start();
}
+ private View findTaskbarTargetForIconView(@NonNull View iconView) {
+ Object tag = iconView.getTag();
+ if (tag instanceof ItemInfo) {
+ ItemInfo item = (ItemInfo) tag;
+ TaskbarViewController taskbarViewController = mControllers.taskbarViewController;
+ if (item.container == CONTAINER_ALL_APPS || item.container == CONTAINER_PREDICTION) {
+ // Since all apps closes when the drag starts, target the all apps button instead.
+ return taskbarViewController.getAllAppsButtonView();
+ } else if (item.container >= 0) {
+ // Since folders close when the drag starts, target the folder icon instead.
+ Predicate<ItemInfo> matcher = ItemInfoMatcher.forFolderMatch(
+ ItemInfoMatcher.ofItemIds(IntSet.wrap(item.id)));
+ return taskbarViewController.getFirstIconMatch(matcher);
+ } else if (item.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
+ // Find first icon with same package/user as the deep shortcut.
+ Predicate<ItemInfo> packageUserMatcher = ItemInfoMatcher.ofPackages(
+ Collections.singleton(item.getTargetPackage()), item.user);
+ return taskbarViewController.getFirstIconMatch(packageUserMatcher);
+ }
+ }
+ return iconView;
+ }
+
+ private void setupReturnDragAnimator(float fromX, float fromY, View originalView,
+ TaskbarReturnPropertiesListener animListener) {
+ // Finish any pending return animation before starting a new return
+ if (mReturnAnimator != null) {
+ mReturnAnimator.end();
+ }
+
+ // For top level icons, the target is the icon itself
+ View target = findTaskbarTargetForIconView(originalView);
+
+ int[] toPosition = target.getLocationOnScreen();
+ float toScale = (float) target.getWidth() / mDragIconSize;
+ float toAlpha = (target == originalView) ? 1f : 0f;
+ MultiValueUpdateListener listener = new MultiValueUpdateListener() {
+ final FloatProp mDx = new FloatProp(fromX, toPosition[0], 0,
+ ANIM_DURATION_RETURN_ICON_TO_TASKBAR, Interpolators.FAST_OUT_SLOW_IN);
+ final FloatProp mDy = new FloatProp(fromY, toPosition[1], 0,
+ ANIM_DURATION_RETURN_ICON_TO_TASKBAR,
+ FAST_OUT_SLOW_IN);
+ final FloatProp mScale = new FloatProp(1f, toScale, 0,
+ ANIM_DURATION_RETURN_ICON_TO_TASKBAR, FAST_OUT_SLOW_IN);
+ final FloatProp mAlpha = new FloatProp(1f, toAlpha, 0,
+ ANIM_DURATION_RETURN_ICON_TO_TASKBAR, Interpolators.ACCEL_2);
+ @Override
+ public void onUpdate(float percent, boolean initOnly) {
+ animListener.updateDragShadow(mDx.value, mDy.value, mScale.value, mAlpha.value);
+ }
+ };
+ mReturnAnimator = ValueAnimator.ofFloat(0f, 1f);
+ mReturnAnimator.setDuration(ANIM_DURATION_RETURN_ICON_TO_TASKBAR);
+ mReturnAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ mReturnAnimator.addUpdateListener(listener);
+ }
+
@Override
protected float getX(MotionEvent ev) {
// We will resize to fill the screen while dragging, so use screen coordinates. This ensures
@@ -540,7 +614,7 @@
@Override
protected void exitDrag() {
- if (mDragObject != null) {
+ if (mDragObject != null && !mDisallowGlobalDrag) {
mActivity.getDragLayer().removeView(mDragObject.dragView);
}
}
@@ -556,6 +630,10 @@
return null;
}
+ interface TaskbarReturnPropertiesListener {
+ void updateDragShadow(float x, float y, float scale, float alpha);
+ }
+
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "TaskbarDragController:");
@@ -566,5 +644,7 @@
pw.println(prefix + "\tmRegistrationY=" + mRegistrationY);
pw.println(prefix + "\tmIsSystemDragInProgress=" + mIsSystemDragInProgress);
pw.println(prefix + "\tisInternalDragInProgess=" + super.isDragging());
+ pw.println(prefix + "\tmDisallowGlobalDrag=" + mDisallowGlobalDrag);
+ pw.println(prefix + "\tmDisallowLongClick=" + mDisallowLongClick);
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index 7e75779..d0059f7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -116,6 +116,22 @@
}
@Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (mControllerCallbacks != null) {
+ mControllerCallbacks.tryStashBasedOnMotionEvent(ev);
+ }
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mControllerCallbacks != null) {
+ mControllerCallbacks.tryStashBasedOnMotionEvent(ev);
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ @Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
if (mControllerCallbacks != null) {
@@ -150,6 +166,23 @@
invalidate();
}
+ /**
+ * Sets the roundness of the round corner above Taskbar.
+ * @param cornerRoundness 0 has no round corner, 1 has complete round corner.
+ */
+ protected void setCornerRoundness(float cornerRoundness) {
+ mBackgroundRenderer.setCornerRoundness(cornerRoundness);
+ invalidate();
+ }
+
+ /*
+ * Sets the translation of the background during the swipe up gesture.
+ */
+ protected void setBackgroundTranslationYForSwipe(float translationY) {
+ mBackgroundRenderer.setTranslationYForSwipe(translationY);
+ invalidate();
+ }
+
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 353f1e0..e54fc00 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -18,11 +18,14 @@
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
+import android.view.MotionEvent;
import android.view.ViewTreeObserver;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
+import com.android.launcher3.testing.shared.ResourceUtils;
import com.android.launcher3.util.DimensionUtils;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.TouchController;
import com.android.quickstep.AnimatedFloat;
@@ -31,11 +34,13 @@
/**
* Handles properties/data collection, then passes the results to TaskbarDragLayer to render.
*/
-public class TaskbarDragLayerController implements TaskbarControllers.LoggableTaskbarController {
+public class TaskbarDragLayerController implements TaskbarControllers.LoggableTaskbarController,
+ TaskbarControllers.BackgroundRendererController {
private final TaskbarActivityContext mActivity;
private final TaskbarDragLayer mTaskbarDragLayer;
private final int mFolderMargin;
+ private float mGestureHeightYThreshold;
// Alpha properties for taskbar background.
private final AnimatedFloat mBgTaskbar = new AnimatedFloat(this::updateBackgroundAlpha);
@@ -62,6 +67,7 @@
mTaskbarDragLayer = taskbarDragLayer;
final Resources resources = mTaskbarDragLayer.getResources();
mFolderMargin = resources.getDimensionPixelSize(R.dimen.taskbar_folder_margin);
+ updateGestureHeight();
}
public void init(TaskbarControllers controllers) {
@@ -121,6 +127,19 @@
return mBgOffset;
}
+ private void updateGestureHeight() {
+ int gestureHeight = ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
+ mActivity.getResources());
+ mGestureHeightYThreshold = mActivity.getDeviceProfile().heightPx - gestureHeight;
+ }
+
+ /**
+ * Make updates when configuration changes.
+ */
+ public void onConfigurationChanged() {
+ updateGestureHeight();
+ }
+
private void updateBackgroundAlpha() {
final float bgNavbar = mBgNavbar.value;
final float bgTaskbar = mBgTaskbar.value * mKeyguardBgTaskbar.value
@@ -131,12 +150,24 @@
updateNavBarDarkIntensityMultiplier();
}
+ /**
+ * Sets the translation of the background during the swipe up gesture.
+ */
+ public void setTranslationYForSwipe(float transY) {
+ mTaskbarDragLayer.setBackgroundTranslationYForSwipe(transY);
+ }
+
private void updateBackgroundOffset() {
mTaskbarDragLayer.setTaskbarBackgroundOffset(mBgOffset.value);
updateNavBarDarkIntensityMultiplier();
}
+ @Override
+ public void setCornerRoundness(float cornerRoundness) {
+ mTaskbarDragLayer.setCornerRoundness(cornerRoundness);
+ }
+
private void updateNavBarDarkIntensityMultiplier() {
// Zero out the app-requested dark intensity when we're drawing our own background.
float effectiveBgAlpha = mLastSetBackgroundAlpha * (1 - mBgOffset.value);
@@ -157,6 +188,8 @@
*/
public class TaskbarDragLayerCallbacks {
+ private final int[] mTempOutLocation = new int[2];
+
/**
* Called to update the touchable insets.
* @see ViewTreeObserver.InternalInsetsInfo#setTouchableInsets(int)
@@ -166,10 +199,41 @@
}
/**
+ * Listens to TaskbarDragLayer touch events and responds accordingly.
+ */
+ public void tryStashBasedOnMotionEvent(MotionEvent ev) {
+ if (!DisplayController.isTransientTaskbar(mActivity)) {
+ return;
+ }
+ if (mControllers.taskbarStashController.isStashed()) {
+ return;
+ }
+
+ boolean stashTaskbar = false;
+
+ MotionEvent screenCoordinates = MotionEvent.obtain(ev);
+ if (ev.getAction() == MotionEvent.ACTION_OUTSIDE) {
+ stashTaskbar = true;
+ } else if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mTaskbarDragLayer.getLocationOnScreen(mTempOutLocation);
+ screenCoordinates.offsetLocation(mTempOutLocation[0], mTempOutLocation[1]);
+
+ if (!mControllers.taskbarViewController.isEventOverAnyItem(screenCoordinates)
+ && screenCoordinates.getY() < mGestureHeightYThreshold) {
+ stashTaskbar = true;
+ }
+ }
+
+ if (stashTaskbar) {
+ mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
+ }
+ }
+
+ /**
* Called when a child is removed from TaskbarDragLayer.
*/
public void onDragLayerViewRemoved() {
- mActivity.maybeSetTaskbarWindowNotFullscreen();
+ mActivity.onDragEndOrViewRemoved();
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java
index 16cc0ac..365ec75 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduController.java
@@ -219,7 +219,7 @@
}
int getIconLayoutBoundsWidth() {
- return mControllers.taskbarViewController.getIconLayoutBounds().width();
+ return mControllers.taskbarViewController.getIconLayoutWidth();
}
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 63f1486..fc26f5f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -38,6 +38,7 @@
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.uioverrides.states.OverviewState;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.quickstep.AnimatedFloat;
import com.android.quickstep.RecentsAnimationCallbacks;
@@ -72,6 +73,7 @@
private TaskbarControllers mControllers;
private AnimatedFloat mTaskbarBackgroundAlpha;
+ private AnimatedFloat mTaskbarCornerRoundness;
private MultiProperty mIconAlphaForHome;
private QuickstepLauncher mLauncher;
@@ -117,6 +119,10 @@
mLauncherState = finalState;
updateStateForFlag(FLAG_TRANSITION_STATE_RUNNING, false);
applyState();
+ mControllers.taskbarDragController.setDisallowGlobalDrag(
+ (finalState instanceof OverviewState));
+ mControllers.taskbarDragController.setDisallowLongClick(
+ finalState == LauncherState.OVERVIEW_SPLIT_SELECT);
}
};
@@ -128,6 +134,7 @@
mTaskbarBackgroundAlpha = mControllers.taskbarDragLayerController
.getTaskbarBackgroundAlpha();
+ mTaskbarCornerRoundness = mControllers.getTaskbarCornerRoundness();
mIconAlphaForHome = mControllers.taskbarViewController
.getTaskbarIconAlpha().get(ALPHA_INDEX_HOME);
@@ -245,17 +252,7 @@
private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) {
boolean goingToLauncher = isInLauncher();
- final float toAlignment;
- if (goingToLauncher) {
- boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
- boolean willStashVisually = isInStashedState
- && mControllers.taskbarStashController.supportsVisualStashing();
- boolean isTaskbarAlignedWithHotseat =
- mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
- toAlignment = isTaskbarAlignedWithHotseat && !willStashVisually ? 1 : 0;
- } else {
- toAlignment = 0;
- }
+ final float toAlignment = isIconAlignedWithHotseat() ? 1 : 0;
if (DEBUG) {
Log.d(TAG, "onStateChangeApplied - mState: " + getStateString(mState)
+ ", changedFlags: " + getStateString(changedFlags)
@@ -321,6 +318,19 @@
.setDuration(duration));
}
+ float cornerRoundness = goingToLauncher ? 0 : 1;
+ // Don't animate if corner roundness has reached desired value.
+ if (mTaskbarCornerRoundness.isAnimating()
+ || mTaskbarCornerRoundness.value != cornerRoundness) {
+ mTaskbarCornerRoundness.cancelAnimation();
+ if (DEBUG) {
+ Log.d(TAG, "onStateChangeApplied - taskbarCornerRoundness - "
+ + mTaskbarCornerRoundness.value
+ + " -> " + cornerRoundness + ": " + duration);
+ }
+ animatorSet.play(mTaskbarCornerRoundness.animateToValue(cornerRoundness));
+ }
+
if (mIconAlignment.isAnimatingToValue(toAlignment)
|| mIconAlignment.isSettledOnValue(toAlignment)) {
// Already at desired value, but make sure we run the callback at the end.
@@ -338,6 +348,7 @@
}
animatorSet.play(iconAlignAnim);
}
+
animatorSet.setInterpolator(EMPHASIZED);
if (start) {
@@ -351,6 +362,22 @@
return mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
}
+ /**
+ * Returns if icons should be aligned to hotseat in the current transition
+ */
+ public boolean isIconAlignedWithHotseat() {
+ if (isInLauncher()) {
+ boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
+ boolean willStashVisually = isInStashedState
+ && mControllers.taskbarStashController.supportsVisualStashing();
+ boolean isTaskbarAlignedWithHotseat =
+ mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
+ return isTaskbarAlignedWithHotseat && !willStashVisually;
+ } else {
+ return false;
+ }
+ }
+
private void playStateTransitionAnim(AnimatorSet animatorSet, long duration,
boolean committed) {
boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
@@ -389,7 +416,7 @@
|| (!taskbarWillBeVisible && Float.compare(currentValue, 0) != 0);
mControllers.taskbarViewController.setLauncherIconAlignment(
- mIconAlignment.value, mIconAlignment.getEndValue(), mLauncher.getDeviceProfile());
+ mIconAlignment.value, mLauncher.getDeviceProfile());
mControllers.navbarButtonsViewController.updateTaskbarAlignment(mIconAlignment.value);
// Switch taskbar and hotseat in last frame
updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java
index 1d3757f..cdc6d59 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimView.java
@@ -69,4 +69,13 @@
mRenderer.getPaint().setAlpha((int) (alpha * 255));
invalidate();
}
+
+ /**
+ * Sets the roundness of the round corner above Taskbar.
+ * @param cornerRoundness 0 has no round corner, 1 has complete round corner.
+ */
+ protected void setCornerRoundness(float cornerRoundness) {
+ mRenderer.setCornerRoundness(cornerRoundness);
+ invalidate();
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index c3b0f57..ce191b7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -30,7 +30,8 @@
/**
* Handles properties/data collection, and passes the results to {@link TaskbarScrimView} to render.
*/
-public class TaskbarScrimViewController implements TaskbarControllers.LoggableTaskbarController {
+public class TaskbarScrimViewController implements TaskbarControllers.LoggableTaskbarController,
+ TaskbarControllers.BackgroundRendererController {
private static final float SCRIM_ALPHA = 0.6f;
@@ -95,6 +96,11 @@
}
@Override
+ public void setCornerRoundness(float cornerRoundness) {
+ mScrimView.setCornerRoundness(cornerRoundness);
+ }
+
+ @Override
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "TaskbarScrimViewController:");
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 64eb99e..72ae1d1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -39,11 +39,13 @@
import androidx.annotation.NonNull;
import com.android.internal.jank.InteractionJankMonitor;
+import com.android.launcher3.Alarm;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.quickstep.AnimatedFloat;
import com.android.quickstep.SystemUiProxy;
@@ -70,6 +72,7 @@
public static final int FLAG_STASHED_IN_TASKBAR_ALL_APPS = 1 << 7; // All apps is visible.
public static final int FLAG_IN_SETUP = 1 << 8; // In the Setup Wizard
public static final int FLAG_STASHED_SMALL_SCREEN = 1 << 9; // phone screen gesture nav, stashed
+ public static final int FLAG_STASHED_IN_APP_AUTO = 1 << 10; // Autohide (transient taskbar).
// If any of these flags are enabled, isInApp should return true.
private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
@@ -78,7 +81,7 @@
private static final int FLAGS_STASHED_IN_APP = FLAG_STASHED_IN_APP_MANUAL
| FLAG_STASHED_IN_APP_PINNED | FLAG_STASHED_IN_APP_EMPTY | FLAG_STASHED_IN_APP_SETUP
| FLAG_STASHED_IN_APP_IME | FLAG_STASHED_IN_TASKBAR_ALL_APPS
- | FLAG_STASHED_SMALL_SCREEN;
+ | FLAG_STASHED_SMALL_SCREEN | FLAG_STASHED_IN_APP_AUTO;
private static final int FLAGS_STASHED_IN_APP_IGNORING_IME =
FLAGS_STASHED_IN_APP & ~FLAG_STASHED_IN_APP_IME;
@@ -132,6 +135,9 @@
*/
private static final boolean DEFAULT_STASHED_PREF = false;
+ // Auto stashes when user has not interacted with the Taskbar after X ms.
+ private static final long NO_TOUCH_TIMEOUT_TO_STASH_MS = 5000;
+
private final TaskbarActivityContext mActivity;
private final SharedPreferences mPrefs;
private final int mStashedHeight;
@@ -162,6 +168,8 @@
private boolean mEnableManualStashingDuringTests = false;
+ private final Alarm mTimeoutAlarm = new Alarm();
+
// Evaluate whether the handle should be stashed
private final StatePropertyHolder mStatePropertyHolder = new StatePropertyHolder(
flags -> {
@@ -182,8 +190,13 @@
if (isPhoneMode()) {
// DeviceProfile's taskbar vars aren't initialized w/ the flag off
Resources resources = mActivity.getResources();
- mUnstashedHeight = resources.getDimensionPixelSize(R.dimen.taskbar_size);
- mStashedHeight = resources.getDimensionPixelOffset(R.dimen.taskbar_stashed_size);
+ boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
+ mUnstashedHeight = resources.getDimensionPixelSize(isTransientTaskbar
+ ? R.dimen.transient_taskbar_size
+ : R.dimen.taskbar_size);
+ mStashedHeight = resources.getDimensionPixelSize(isTransientTaskbar
+ ? R.dimen.transient_taskbar_stashed_size
+ : R.dimen.taskbar_stashed_size);
} else {
mUnstashedHeight = mActivity.getDeviceProfile().taskbarSize;
mStashedHeight = mActivity.getDeviceProfile().stashedTaskbarSize;
@@ -210,13 +223,16 @@
StashedHandleViewController.ALPHA_INDEX_STASHED);
mTaskbarStashedHandleHintScale = stashedHandleController.getStashedHandleHintScale();
+ boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
// We use supportsVisualStashing() here instead of supportsManualStashing() because we want
// it to work properly for tests that recreate taskbar. This check is here just to ensure
// that taskbar unstashes when going to 3 button mode (supportsVisualStashing() false).
boolean isManuallyStashedInApp = supportsVisualStashing()
- && mPrefs.getBoolean(SHARED_PREFS_STASHED_KEY, DEFAULT_STASHED_PREF);
+ && mPrefs.getBoolean(SHARED_PREFS_STASHED_KEY, DEFAULT_STASHED_PREF)
+ && !isTransientTaskbar;
boolean isInSetup = !mActivity.isUserSetupComplete() || setupUIVisible;
updateStateForFlag(FLAG_STASHED_IN_APP_MANUAL, isManuallyStashedInApp);
+ updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, isTransientTaskbar);
updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, isInSetup);
updateStateForFlag(FLAG_IN_SETUP, isInSetup);
updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, isPhoneMode()
@@ -243,7 +259,8 @@
protected boolean supportsManualStashing() {
return supportsVisualStashing()
&& isInApp()
- && (!Utilities.IS_RUNNING_IN_TEST_HARNESS || mEnableManualStashingDuringTests);
+ && (!Utilities.IS_RUNNING_IN_TEST_HARNESS || mEnableManualStashingDuringTests)
+ && !DisplayController.isTransientTaskbar(mActivity);
}
/**
@@ -332,7 +349,8 @@
* @see WindowInsets.Type#systemBars()
*/
public int getContentHeightToReportToApps() {
- if (isPhoneMode() && !mActivity.isThreeButtonNav()) {
+ if ((isPhoneMode() && !mActivity.isThreeButtonNav())
+ || DisplayController.isTransientTaskbar(mActivity)) {
return getStashedHeight();
}
@@ -377,6 +395,20 @@
}
/**
+ * Stash or unstashes the transient taskbar.
+ */
+ public void updateAndAnimateTransientTaskbar(boolean stash) {
+ if (!DisplayController.isTransientTaskbar(mActivity)) {
+ return;
+ }
+
+ if (hasAnyFlag(FLAG_STASHED_IN_APP_AUTO) != stash) {
+ updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, stash);
+ applyState();
+ }
+ }
+
+ /**
* Should be called when long pressing the nav region when taskbar is present.
* @return Whether taskbar was stashed and now is unstashed.
*/
@@ -549,11 +581,17 @@
public void onAnimationStart(Animator animation) {
mIsStashed = isStashed;
onIsStashedChanged(mIsStashed);
+
+ cancelTimeoutIfExists();
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimator = null;
+
+ if (!mIsStashed) {
+ tryStartTaskbarTimeout();
+ }
}
});
}
@@ -779,6 +817,54 @@
mControllers.rotationButtonController.onTaskbarStateChange(visible, stashed);
}
+ /**
+ * Cancels a timeout if any exists.
+ */
+ public void cancelTimeoutIfExists() {
+ if (mTimeoutAlarm.alarmPending()) {
+ mTimeoutAlarm.cancelAlarm();
+ }
+ }
+
+ /**
+ * Updates the status of the taskbar timeout.
+ * @param isAutohideSuspended If true, cancels any existing timeout
+ * If false, attempts to re/start the timeout
+ */
+ public void updateTaskbarTimeout(boolean isAutohideSuspended) {
+ if (!DisplayController.isTransientTaskbar(mActivity)) {
+ return;
+ }
+ if (isAutohideSuspended) {
+ cancelTimeoutIfExists();
+ } else {
+ tryStartTaskbarTimeout();
+ }
+ }
+
+ /**
+ * Attempts to start timer to auto hide the taskbar based on time.
+ */
+ public void tryStartTaskbarTimeout() {
+ if (!DisplayController.isTransientTaskbar(mActivity)) {
+ return;
+ }
+ if (mIsStashed) {
+ return;
+ }
+ cancelTimeoutIfExists();
+
+ mTimeoutAlarm.setOnAlarmListener(this::onTaskbarTimeout);
+ mTimeoutAlarm.setAlarm(NO_TOUCH_TIMEOUT_TO_STASH_MS);
+ }
+
+ private void onTaskbarTimeout(Alarm alarm) {
+ if (mControllers.taskbarAutohideSuspendController.isSuspended()) {
+ return;
+ }
+ updateAndAnimateTransientTaskbar(true);
+ }
+
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "TaskbarStashController:");
@@ -794,17 +880,18 @@
}
private static String getStateString(int flags) {
- StringJoiner str = new StringJoiner("|");
- appendFlag(str, flags, FLAGS_IN_APP, "FLAG_IN_APP");
- appendFlag(str, flags, FLAG_STASHED_IN_APP_MANUAL, "FLAG_STASHED_IN_APP_MANUAL");
- appendFlag(str, flags, FLAG_STASHED_IN_APP_PINNED, "FLAG_STASHED_IN_APP_PINNED");
- appendFlag(str, flags, FLAG_STASHED_IN_APP_EMPTY, "FLAG_STASHED_IN_APP_EMPTY");
- appendFlag(str, flags, FLAG_STASHED_IN_APP_SETUP, "FLAG_STASHED_IN_APP_SETUP");
- appendFlag(str, flags, FLAG_STASHED_IN_APP_IME, "FLAG_STASHED_IN_APP_IME");
- appendFlag(str, flags, FLAG_IN_STASHED_LAUNCHER_STATE, "FLAG_IN_STASHED_LAUNCHER_STATE");
- appendFlag(str, flags, FLAG_STASHED_IN_TASKBAR_ALL_APPS, "FLAG_STASHED_IN_APP_ALL_APPS");
- appendFlag(str, flags, FLAG_IN_SETUP, "FLAG_IN_SETUP");
- return str.toString();
+ StringJoiner sj = new StringJoiner("|");
+ appendFlag(sj, flags, FLAGS_IN_APP, "FLAG_IN_APP");
+ appendFlag(sj, flags, FLAG_STASHED_IN_APP_MANUAL, "FLAG_STASHED_IN_APP_MANUAL");
+ appendFlag(sj, flags, FLAG_STASHED_IN_APP_PINNED, "FLAG_STASHED_IN_APP_PINNED");
+ appendFlag(sj, flags, FLAG_STASHED_IN_APP_EMPTY, "FLAG_STASHED_IN_APP_EMPTY");
+ appendFlag(sj, flags, FLAG_STASHED_IN_APP_SETUP, "FLAG_STASHED_IN_APP_SETUP");
+ appendFlag(sj, flags, FLAG_STASHED_IN_APP_IME, "FLAG_STASHED_IN_APP_IME");
+ appendFlag(sj, flags, FLAG_IN_STASHED_LAUNCHER_STATE, "FLAG_IN_STASHED_LAUNCHER_STATE");
+ appendFlag(sj, flags, FLAG_STASHED_IN_TASKBAR_ALL_APPS, "FLAG_STASHED_IN_TASKBAR_ALL_APPS");
+ appendFlag(sj, flags, FLAG_IN_SETUP, "FLAG_IN_SETUP");
+ appendFlag(sj, flags, FLAG_STASHED_IN_APP_AUTO, "FLAG_STASHED_IN_APP_AUTO");
+ return sj.toString();
}
private class StatePropertyHolder {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
new file mode 100644
index 0000000..1a7ec13
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2022 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.taskbar;
+
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.quickstep.AnimatedFloat.VALUE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.util.DisplayController;
+import com.android.quickstep.AnimatedFloat;
+
+import java.io.PrintWriter;
+
+/**
+ * Class responsible for translating the transient taskbar UI during a swipe gesture.
+ *
+ * The translation is controlled, in priority order:
+ * - animation to home
+ * - a spring animation
+ * - controlled by user
+ *
+ * The spring animation will play start once the user lets go or when user pauses to go to overview.
+ * When the user goes home, the stash animation will play.
+ */
+public class TaskbarTranslationController implements TaskbarControllers.LoggableTaskbarController {
+
+ private final TaskbarActivityContext mContext;
+ private TaskbarControllers mControllers;
+ private final AnimatedFloat mTranslationYForSwipe = new AnimatedFloat(
+ this::updateTranslationYForSwipe);
+
+ private boolean mHasSprungOnceThisGesture;
+ private @Nullable ValueAnimator mSpringBounce;
+ private boolean mGestureEnded;
+ private boolean mAnimationToHomeRunning;
+
+ private final boolean mIsTransientTaskbar;
+
+ private final TransitionCallback mCallback;
+
+ public TaskbarTranslationController(TaskbarActivityContext context) {
+ mContext = context;
+ mIsTransientTaskbar = DisplayController.isTransientTaskbar(mContext);
+ mCallback = new TransitionCallback();
+ }
+
+ /**
+ * Initialization method.
+ */
+ public void init(TaskbarControllers controllers) {
+ mControllers = controllers;
+ }
+
+ /**
+ * Called to cancel any existing animations.
+ */
+ public void cancelAnimationIfExists() {
+ if (mSpringBounce != null) {
+ mSpringBounce.cancel();
+ mSpringBounce = null;
+ }
+ reset();
+ }
+
+ private void updateTranslationYForSwipe() {
+ if (!mIsTransientTaskbar) {
+ return;
+ }
+
+ float transY = mTranslationYForSwipe.value;
+ mControllers.stashedHandleViewController.setTranslationYForSwipe(transY);
+ mControllers.taskbarViewController.setTranslationYForSwipe(transY);
+ mControllers.taskbarDragLayerController.setTranslationYForSwipe(transY);
+ }
+
+ /**
+ * Starts a spring aniamtion to set the views back to the resting state.
+ */
+ public void startSpring() {
+ if (mHasSprungOnceThisGesture || mAnimationToHomeRunning) {
+ return;
+ }
+ mSpringBounce = new SpringAnimationBuilder(mContext)
+ .setStartValue(mTranslationYForSwipe.value)
+ .setEndValue(0)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+ .setStiffness(SpringForce.STIFFNESS_LOW)
+ .build(mTranslationYForSwipe, VALUE);
+ mSpringBounce.addListener(forEndCallback(() -> {
+ if (mGestureEnded) {
+ reset();
+ }
+ }));
+ mSpringBounce.start();
+ mHasSprungOnceThisGesture = true;
+ }
+
+ private void reset() {
+ mGestureEnded = false;
+ mHasSprungOnceThisGesture = false;
+ }
+
+ /**
+ * Returns a callback to help monitor the swipe gesture.
+ */
+ public TransitionCallback getTransitionCallback() {
+ return mCallback;
+ }
+
+ /**
+ * Returns an animation to reset the taskbar translation for animation back to launcher.
+ */
+ public ObjectAnimator createAnimToLauncher(long duration) {
+ ObjectAnimator animator = ObjectAnimator.ofFloat(mTranslationYForSwipe, VALUE, 0);
+ animator.setInterpolator(Interpolators.LINEAR);
+ animator.setDuration(duration);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ cancelAnimationIfExists();
+ mAnimationToHomeRunning = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimationToHomeRunning = false;
+ reset();
+ }
+ });
+ return animator;
+ }
+
+ /**
+ * Helper class to communicate to/from the input consumer.
+ */
+ public class TransitionCallback {
+
+ /**
+ * Called when there is movement to move the taskbar.
+ */
+ public void onActionMove(float dY) {
+ if (mAnimationToHomeRunning
+ || (mHasSprungOnceThisGesture && !mGestureEnded)) {
+ return;
+ }
+
+ mTranslationYForSwipe.updateValue(dY);
+ }
+
+ /**
+ * Called when swipe gesture has ended.
+ */
+ public void onActionEnd() {
+ if (mHasSprungOnceThisGesture) {
+ reset();
+ } else {
+ mGestureEnded = true;
+ startSpring();
+ }
+ }
+ }
+
+ @Override
+ public void dumpLogs(String prefix, PrintWriter pw) {
+ pw.println(prefix + "TaskbarTranslationController:");
+
+ pw.println(prefix + "\tmTranslationYForSwipe=" + mTranslationYForSwipe.value);
+ pw.println(prefix + "\tmHasSprungOnceThisGesture=" + mHasSprungOnceThisGesture);
+ pw.println(prefix + "\tmAnimationToHomeRunning=" + mAnimationToHomeRunning);
+ pw.println(prefix + "\tmGestureEnded=" + mGestureEnded);
+ pw.println(prefix + "\tmSpringBounce is running=" + (mSpringBounce != null
+ && mSpringBounce.isRunning()));
+ }
+}
+
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 49dba95..1014cb6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.taskbar;
+import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.CallSuper;
@@ -97,6 +98,36 @@
}
}
+ /**
+ * Returns true iff taskbar is stashed.
+ */
+ public boolean isTaskbarStashed() {
+ return mControllers.taskbarStashController.isStashed();
+ }
+
+ /**
+ * Called at the end of the swipe gesture on Transient taskbar.
+ */
+ public void startTranslationSpring() {
+ mControllers.taskbarActivityContext.startTranslationSpring();
+ }
+
+ /*
+ * @param ev MotionEvent in screen coordinates.
+ * @return Whether any Taskbar item could handle the given MotionEvent if given the chance.
+ */
+ public boolean isEventOverAnyTaskbarItem(MotionEvent ev) {
+ return mControllers.taskbarViewController.isEventOverAnyItem(ev)
+ || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev);
+ }
+
+ /**
+ * Returns true if icons should be aligned to hotseat in the current transition.
+ */
+ public boolean isIconAlignedWithHotseat() {
+ return false;
+ }
+
@CallSuper
protected void dumpLogs(String prefix, PrintWriter pw) {
pw.println(String.format(
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index c754a59..fe38bb1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -16,14 +16,11 @@
package com.android.launcher3.taskbar;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
-import android.os.SystemProperties;
import android.util.AttributeSet;
-import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
@@ -85,12 +82,6 @@
private View mQsb;
- // Only non-null when device supports having a floating task.
- private @Nullable View mFloatingTaskButton;
- private @Nullable Intent mFloatingTaskIntent;
- private static final boolean FLOATING_TASKS_ENABLED =
- SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false);
-
public TaskbarView(@NonNull Context context) {
this(context, null);
}
@@ -139,17 +130,6 @@
// TODO: Disable touch events on QSB otherwise it can crash.
mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
- if (FLOATING_TASKS_ENABLED) {
- mFloatingTaskIntent = FloatingTaskIntentResolver.getIntent(context);
- if (mFloatingTaskIntent != null) {
- mFloatingTaskButton = LayoutInflater.from(context)
- .inflate(R.layout.taskbar_floating_task_button, this, false);
- mFloatingTaskButton.setPadding(mItemPadding, mItemPadding, mItemPadding,
- mItemPadding);
- } else {
- Log.d(TAG, "Floating tasks is enabled but no intent was found!");
- }
- }
}
private int getColorWithGivenLuminance(int color, float luminance) {
@@ -177,10 +157,6 @@
if (mAllAppsButton != null) {
mAllAppsButton.setOnClickListener(mControllerCallbacks.getAllAppsButtonClickListener());
}
- if (mFloatingTaskButton != null) {
- mFloatingTaskButton.setOnClickListener(
- mControllerCallbacks.getFloatingTaskButtonListener(mFloatingTaskIntent));
- }
}
private void removeAndRecycle(View view) {
@@ -205,9 +181,6 @@
}
removeView(mQsb);
- if (mFloatingTaskButton != null) {
- removeView(mFloatingTaskButton);
- }
for (int i = 0; i < hotseatItemInfos.length; i++) {
ItemInfo hotseatItemInfo = hotseatItemInfos[i];
@@ -290,11 +263,6 @@
mQsb.setVisibility(View.INVISIBLE);
}
- if (mFloatingTaskButton != null) {
- int index = Utilities.isRtl(getResources()) ? 0 : getChildCount();
- addView(mFloatingTaskButton, index);
- }
-
mThemeIconsBackground = calculateThemeIconsBackground();
setThemedIconsBackgroundColor(mThemeIconsBackground);
}
@@ -325,12 +293,8 @@
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int count = getChildCount();
- int countExcludingQsb = count;
DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
- if (deviceProfile.isQsbInline) {
- countExcludingQsb--;
- }
- int spaceNeeded = countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize);
+ int spaceNeeded = getIconLayoutWidth();
int navSpaceNeeded = deviceProfile.hotseatBarEndOffset;
boolean layoutRtl = isLayoutRtl();
int iconEnd = right - (right - left - spaceNeeded) / 2;
@@ -381,12 +345,22 @@
}
@Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ mControllerCallbacks.onInterceptTouchEvent(ev);
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ @Override
public boolean onTouchEvent(MotionEvent event) {
if (!mTouchEnabled) {
return true;
}
- if (mIconLayoutBounds.left <= event.getX() && event.getX() <= mIconLayoutBounds.right) {
- // Don't allow long pressing between icons, or above/below them.
+ if (mIconLayoutBounds.left <= event.getX()
+ && event.getX() <= mIconLayoutBounds.right
+ && !DisplayController.isTransientTaskbar(mActivityContext)) {
+ // Don't allow long pressing between icons, or above/below them
+ // unless its transient taskbar.
+ mControllerCallbacks.clearTouchInProgress();
return true;
}
if (mControllerCallbacks.onTouchEvent(event)) {
@@ -403,6 +377,7 @@
public void setTouchesEnabled(boolean touchEnabled) {
this.mTouchEnabled = touchEnabled;
+ mControllerCallbacks.clearTouchInProgress();
}
/**
@@ -421,6 +396,18 @@
}
/**
+ * Returns the space used by the icons
+ */
+ public int getIconLayoutWidth() {
+ int countExcludingQsb = getChildCount();
+ DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
+ if (deviceProfile.isQsbInline) {
+ countExcludingQsb--;
+ }
+ return countExcludingQsb * (mItemMarginLeftRight * 2 + mIconTouchSize);
+ }
+
+ /**
* Returns the app icons currently shown in the taskbar.
*/
public View[] getIconViews() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index f0277a4..80a31b4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -22,16 +22,18 @@
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP;
import static com.android.launcher3.taskbar.TaskbarManager.isPhoneMode;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
import static com.android.quickstep.AnimatedFloat.VALUE;
import android.annotation.NonNull;
-import android.content.Intent;
import android.graphics.Rect;
import android.util.FloatProperty;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
+import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils;
import androidx.core.view.OneShotPreDrawListener;
@@ -48,6 +50,7 @@
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.icons.ThemedIconDrawable;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.HorizontalInsettableView;
import com.android.launcher3.util.ItemInfoMatcher;
@@ -55,7 +58,6 @@
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.quickstep.AnimatedFloat;
-import com.android.quickstep.SystemUiProxy;
import java.io.PrintWriter;
import java.util.function.Predicate;
@@ -89,6 +91,7 @@
this::updateTranslationY);
private AnimatedFloat mTaskbarNavButtonTranslationY;
private AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay;
+ private float mTaskbarIconTranslationYForSwipe;
private final int mTaskbarBottomMargin;
@@ -97,6 +100,9 @@
private final TaskbarModelCallbacks mModelCallbacks;
+ // Captures swipe down action to close transient taskbar.
+ protected @Nullable SingleAxisSwipeDetector mSwipeDownDetector;
+
// Initialized in init.
private TaskbarControllers mControllers;
@@ -119,6 +125,31 @@
mTaskbarBottomMargin = DisplayController.isTransientTaskbar(activity)
? activity.getResources().getDimensionPixelSize(R.dimen.transient_taskbar_margin)
: 0;
+
+ if (DisplayController.isTransientTaskbar(mActivity)) {
+ mSwipeDownDetector = new SingleAxisSwipeDetector(activity,
+ new SingleAxisSwipeDetector.Listener() {
+ private float mLastDisplacement;
+
+ @Override
+ public boolean onDrag(float displacement) {
+ mLastDisplacement = displacement;
+ return false;
+ }
+
+ @Override
+ public void onDragEnd(float velocity) {
+ if (mLastDisplacement > 0) {
+ mControllers.taskbarStashController
+ .updateAndAnimateTransientTaskbar(true);
+ }
+ }
+
+ @Override
+ public void onDragStart(boolean start, float startDisplacement) {}
+ }, VERTICAL);
+ mSwipeDownDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
+ }
}
public void init(TaskbarControllers controllers) {
@@ -201,6 +232,10 @@
return mTaskbarView.getIconLayoutBounds();
}
+ public int getIconLayoutWidth() {
+ return mTaskbarView.getIconLayoutWidth();
+ }
+
public View[] getIconViews() {
return mTaskbarView.getIconViews();
}
@@ -226,9 +261,18 @@
mTaskbarView.setScaleY(scale);
}
+ /**
+ * Sets the translation of the TaskbarView during the swipe up gesture.
+ */
+ public void setTranslationYForSwipe(float transY) {
+ mTaskbarIconTranslationYForSwipe = transY;
+ updateTranslationY();
+ }
+
private void updateTranslationY() {
mTaskbarView.setTranslationY(mTaskbarIconTranslationYForHome.value
- + mTaskbarIconTranslationYForStash.value);
+ + mTaskbarIconTranslationYForStash.value
+ + mTaskbarIconTranslationYForSwipe);
}
private void updateIconsBackground() {
@@ -246,10 +290,9 @@
* 0 => not aligned
* 1 => fully aligned
*/
- public void setLauncherIconAlignment(float alignmentRatio, Float endAlignment,
- DeviceProfile launcherDp) {
+ public void setLauncherIconAlignment(float alignmentRatio, DeviceProfile launcherDp) {
if (mIconAlignControllerLazy == null) {
- mIconAlignControllerLazy = createIconAlignmentController(launcherDp, endAlignment);
+ mIconAlignControllerLazy = createIconAlignmentController(launcherDp);
}
mIconAlignControllerLazy.setPlayFraction(alignmentRatio);
if (alignmentRatio <= 0 || alignmentRatio >= 1) {
@@ -261,8 +304,7 @@
/**
* Creates an animation for aligning the taskbar icons with the provided Launcher device profile
*/
- private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp,
- Float endAlignment) {
+ private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) {
mOnControllerPreCreateCallback.run();
PendingAnimation setter = new PendingAnimation(100);
DeviceProfile taskbarDp = mActivity.getDeviceProfile();
@@ -288,7 +330,7 @@
setter.addOnFrameListener(anim -> mActivity.setTaskbarWindowHeight(
anim.getAnimatedFraction() > 0 ? expandedHeight : collapsedHeight));
- boolean isToHome = endAlignment != null && endAlignment == 1;
+ boolean isToHome = mControllers.uiController.isIconAlignedWithHotseat();
for (int i = 0; i < mTaskbarView.getChildCount(); i++) {
View child = mTaskbarView.getChildAt(i);
int positionInHotseat;
@@ -436,6 +478,8 @@
private float mDownX, mDownY;
private boolean mCanceledStashHint;
+ private boolean mTouchInProgress;
+
public View.OnClickListener getIconOnClickListener() {
return mActivity.getItemOnClickListener();
}
@@ -447,13 +491,6 @@
};
}
- public View.OnClickListener getFloatingTaskButtonListener(@NonNull Intent intent) {
- return v -> {
- SystemUiProxy proxy = SystemUiProxy.INSTANCE.get(v.getContext());
- proxy.showFloatingTask(intent);
- };
- }
-
public View.OnLongClickListener getIconOnLongClickListener() {
return mControllers.taskbarDragController::startDragOnLongClick;
}
@@ -464,37 +501,75 @@
}
/**
+ * Simply listens to all intercept touch events passed to TaskbarView.
+ */
+ public void onInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mTouchInProgress = true;
+ }
+
+ if (mTouchInProgress && mSwipeDownDetector != null) {
+ mSwipeDownDetector.onTouchEvent(ev);
+ }
+
+ if (ev.getAction() == MotionEvent.ACTION_UP
+ || ev.getAction() == MotionEvent.ACTION_CANCEL) {
+ clearTouchInProgress();
+ }
+ }
+
+ /**
* Get the first chance to handle TaskbarView#onTouchEvent, and return whether we want to
* consume the touch so TaskbarView treats it as an ACTION_CANCEL.
*/
public boolean onTouchEvent(MotionEvent motionEvent) {
+ boolean shouldConsumeTouch = false;
+ boolean clearTouchInProgress = false;
+
final float x = motionEvent.getRawX();
final float y = motionEvent.getRawY();
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
+ mTouchInProgress = true;
mDownX = x;
mDownY = y;
mControllers.taskbarStashController.startStashHint(/* animateForward = */ true);
mCanceledStashHint = false;
break;
case MotionEvent.ACTION_MOVE:
- if (!mCanceledStashHint
+ if (mTouchInProgress
+ && !mCanceledStashHint
&& squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop) {
mControllers.taskbarStashController.startStashHint(
/* animateForward= */ false);
mCanceledStashHint = true;
- return true;
+ shouldConsumeTouch = true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
- if (!mCanceledStashHint) {
+ if (mTouchInProgress && !mCanceledStashHint) {
mControllers.taskbarStashController.startStashHint(
/* animateForward= */ false);
}
+ clearTouchInProgress = true;
break;
}
- return false;
+
+ if (mTouchInProgress && mSwipeDownDetector != null) {
+ mSwipeDownDetector.onTouchEvent(motionEvent);
+ }
+ if (clearTouchInProgress) {
+ clearTouchInProgress();
+ }
+ return shouldConsumeTouch;
+ }
+
+ /**
+ * Ensures that we do not pass any more touch events to the SwipeDetector.
+ */
+ public void clearTouchInProgress() {
+ mTouchInProgress = false;
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
index 837af58..a033507 100644
--- a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
@@ -14,7 +14,8 @@
* Controls Taskbar behavior while Voice Interaction Window (assistant) is showing.
*/
class VoiceInteractionWindowController(val context: TaskbarActivityContext)
- : TaskbarControllers.LoggableTaskbarController {
+ : TaskbarControllers.LoggableTaskbarController,
+ TaskbarControllers.BackgroundRendererController {
private val taskbarBackgroundRenderer = TaskbarBackgroundRenderer(context)
@@ -111,6 +112,11 @@
}
}
+ override fun setCornerRoundness(cornerRoundness: Float) {
+ taskbarBackgroundRenderer.setCornerRoundness(cornerRoundness)
+ separateWindowForTaskbarBackground.invalidate()
+ }
+
override fun dumpLogs(prefix: String, pw: PrintWriter) {
pw.println(prefix + "VoiceInteractionWindowController:")
pw.println("$prefix\tisVoiceInteractionWindowVisible=$isVoiceInteractionWindowVisible")
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
index ea37944..85c6318 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
@@ -16,6 +16,7 @@
package com.android.launcher3.taskbar.allapps;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.R;
import com.android.launcher3.appprediction.PredictionRowView;
@@ -123,4 +124,11 @@
.findFixedRowByType(PredictionRowView.class)
.setPredictedApps(mPredictedApps);
}
+
+
+ @VisibleForTesting
+ public int getTaskbarAllAppsTopPadding() {
+ // Allow null-pointer since this should only be null if the apps view is not showing.
+ return mAppsView.getActiveRecyclerView().getClipBounds().top;
+ }
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index eaf577b..9813c8a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -70,6 +70,7 @@
import android.view.WindowManagerGlobal;
import android.window.SplashScreen;
+import androidx.annotation.BinderThread;
import androidx.annotation.Nullable;
import com.android.app.viewcapture.ViewCapture;
@@ -135,6 +136,7 @@
import com.android.quickstep.util.RemoteAnimationProvider;
import com.android.quickstep.util.RemoteFadeOutAnimationListener;
import com.android.quickstep.util.SplitSelectStateController;
+import com.android.quickstep.util.SplitWithKeyboardShortcutController;
import com.android.quickstep.util.TISBindHelper;
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
@@ -179,6 +181,8 @@
private @Nullable UnfoldTransitionProgressProvider mUnfoldTransitionProgressProvider;
private @Nullable RotationChangeProvider mRotationChangeProvider;
private @Nullable LauncherUnfoldAnimationController mLauncherUnfoldAnimationController;
+
+ private SplitWithKeyboardShortcutController mSplitWithKeyboardShortcutController;
/**
* If Launcher restarted while in the middle of an Overview split select, it needs this data to
* recover. In all other cases this will remain null.
@@ -194,11 +198,13 @@
super.setupViews();
mActionsView = findViewById(R.id.overview_actions_view);
- RecentsView overviewPanel = (RecentsView) getOverviewPanel();
+ RecentsView overviewPanel = getOverviewPanel();
SplitSelectStateController controller =
new SplitSelectStateController(this, mHandler, getStateManager(),
getDepthController(), getStatsLogManager());
overviewPanel.init(mActionsView, controller);
+ mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(this,
+ controller);
mActionsView.updateDimension(getDeviceProfile(), overviewPanel.getLastComputedTaskSize());
mActionsView.updateVerticalMargin(DisplayController.getNavigationMode(this));
@@ -213,6 +219,8 @@
mEnableWidgetDepth = ENABLE_WIDGET_PICKER_DEPTH.get()
&& SystemProperties.getBoolean("ro.launcher.depth.widget", true);
+ getWorkspace().addOverlayCallback(progress ->
+ onTaskbarInAppDisplayProgressUpdate(progress, MINUS_ONE_PAGE_PROGRESS_INDEX));
}
@Override
@@ -321,6 +329,17 @@
super.showAllAppsFromIntent(alreadyOnHome);
}
+ protected void onItemClicked(View view) {
+ if (!mSplitWithKeyboardShortcutController.handleSecondAppSelectionForSplit(view)) {
+ QuickstepLauncher.super.getItemOnClickListener().onClick(view);
+ }
+ }
+
+ @Override
+ public View.OnClickListener getItemOnClickListener() {
+ return this::onItemClicked;
+ }
+
@Override
public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
Stream<SystemShortcut.Factory> base = Stream.of(WellbeingModel.SHORTCUT_FACTORY);
@@ -402,6 +421,7 @@
super.onDestroy();
mHotseatPredictionController.destroy();
+ mSplitWithKeyboardShortcutController.onDestroy();
if (mViewCapture != null) mViewCapture.close();
}
@@ -576,17 +596,6 @@
recentsView.finishRecentsAnimation(true /* toRecents */, null);
}
- /**
- * {@code LauncherOverlayCallbacks} scroll amount.
- * Indicates transition progress to -1 screen.
- * @param progress From 0 to 1.
- */
- @Override
- public void onScrollChanged(float progress) {
- super.onScrollChanged(progress);
- onTaskbarInAppDisplayProgressUpdate(progress, MINUS_ONE_PAGE_PROGRESS_INDEX);
- }
-
@Override
public void onAllAppsTransition(float progress) {
super.onAllAppsTransition(progress);
@@ -832,6 +841,12 @@
return activityOptions;
}
+ @Override
+ @BinderThread
+ public void enterStageSplitFromRunningApp(boolean leftOrTop) {
+ mSplitWithKeyboardShortcutController.enterStageSplit(leftOrTop);
+ }
+
/**
* Adds a new launch cookie for the activity launch if supported.
*
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index bd270f9..b09e531 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -48,7 +48,7 @@
import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
import static com.android.quickstep.util.VibratorWrapper.OVERVIEW_HAPTIC;
import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
@@ -64,6 +64,7 @@
import android.app.WindowConfiguration;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Resources;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -98,9 +99,11 @@
import com.android.launcher3.logging.StatsLogManager.StatsLogger;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.tracing.InputConsumerProto;
import com.android.launcher3.tracing.SwipeHandlerProto;
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.TraceHelper;
import com.android.launcher3.util.WindowBounds;
import com.android.quickstep.BaseActivityInterface.AnimationFactory;
@@ -311,6 +314,10 @@
// Interpolate RecentsView scale from start of quick switch scroll until this scroll threshold
private final float mQuickSwitchScaleScrollThreshold;
+ private final int mTaskbarAppWindowThreshold;
+ private final int mTaskbarCatchUpThreshold;
+ private boolean mTaskbarAlreadyOpen;
+
public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
TaskAnimationManager taskAnimationManager, GestureState gestureState,
long touchTimeMs, boolean continuingLastGesture,
@@ -331,11 +338,17 @@
mTaskAnimationManager = taskAnimationManager;
mTouchTimeMs = touchTimeMs;
mContinuingLastGesture = continuingLastGesture;
- mQuickSwitchScaleScrollThreshold = context.getResources().getDimension(
- R.dimen.quick_switch_scaling_scroll_threshold);
- mSplashMainWindowShiftLength = -context.getResources().getDimensionPixelSize(
- R.dimen.starting_surface_exit_animation_window_shift_length);
+ Resources res = context.getResources();
+ mTaskbarAppWindowThreshold = res
+ .getDimensionPixelSize(R.dimen.taskbar_app_window_threshold);
+ mTaskbarCatchUpThreshold = res.getDimensionPixelSize(R.dimen.taskbar_catch_up_threshold);
+
+ mQuickSwitchScaleScrollThreshold = res
+ .getDimension(R.dimen.quick_switch_scaling_scroll_threshold);
+
+ mSplashMainWindowShiftLength = -res
+ .getDimensionPixelSize(R.dimen.starting_surface_exit_animation_window_shift_length);
initAfterSubclassConstructor();
initStateCallbacks();
@@ -650,6 +663,8 @@
true/* moveFocusedTask */,
new ActiveGestureLog.CompoundString(
"motion pause detected (animate=true)"));
+ Optional.ofNullable(mActivityInterface.getTaskbarController())
+ .ifPresent(TaskbarUIController::startTranslationSpring);
performHapticFeedback();
}
@@ -824,7 +839,7 @@
return;
}
mLauncherTransitionController.setProgress(
- Math.max(mCurrentShift.value, getScaleProgressDueToScroll()), mDragLengthFactor);
+ Math.max(getTaskbarProgress(), getScaleProgressDueToScroll()), mDragLengthFactor);
}
/**
@@ -953,7 +968,6 @@
new ActiveGestureLog.CompoundString("on gesture started (animate=false)"));
mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED);
mGestureStarted = true;
- SystemUiProxy.INSTANCE.get(mContext).notifySwipeUpGestureStarted();
}
/**
@@ -1176,7 +1190,7 @@
}
private boolean hasReachedOverviewThreshold() {
- return mCurrentShift.value > MIN_PROGRESS_FOR_OVERVIEW;
+ return getTaskbarProgress() > MIN_PROGRESS_FOR_OVERVIEW;
}
@UiThread
@@ -1699,10 +1713,6 @@
private void resumeLastTask() {
if (mRecentsAnimationController != null) {
mRecentsAnimationController.finish(false /* toRecents */, null);
- ActiveGestureLog.INSTANCE.addLog(
- /* event= */ "finishRecentsAnimation",
- /* extras= */ false,
- /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
}
doLogGesture(LAST_TASK, null);
reset();
@@ -1911,10 +1921,6 @@
mRecentsAnimationController.detachNavigationBarFromApp(true);
}
}
- ActiveGestureLog.INSTANCE.addLog(
- /* event= */ "finishRecentsAnimation",
- /* extras= */ true,
- /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
}
private void finishCurrentTransitionToHome() {
@@ -1926,10 +1932,6 @@
finishRecentsControllerToHome(
() -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
}
- ActiveGestureLog.INSTANCE.addLog(
- /* event= */ "finishRecentsAnimation",
- /* extras= */ true,
- /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
doLogGesture(HOME, mRecentsView == null ? null : mRecentsView.getCurrentPageTaskView());
}
@@ -2040,6 +2042,9 @@
mGestureState.updateLastStartedTaskId(taskId);
boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
.contains(taskId);
+ if (!hasTaskPreviouslyAppeared) {
+ ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
+ }
nextTask.launchTask(success -> {
resultCallback.accept(success);
if (success) {
@@ -2204,7 +2209,7 @@
AnimatorControllerWithResistance playbackController =
remoteHandle.getPlaybackController();
if (playbackController != null) {
- playbackController.setProgress(Math.max(mCurrentShift.value,
+ playbackController.setProgress(Math.max(getTaskbarProgress(),
getScaleProgressDueToScroll()), mDragLengthFactor);
}
@@ -2248,6 +2253,41 @@
return scaleProgress;
}
+ /**
+ * Updates the current status of taskbar during this swipe.
+ */
+ public void setTaskbarAlreadyOpen(boolean taskbarAlreadyOpen) {
+ mTaskbarAlreadyOpen = taskbarAlreadyOpen;
+ }
+
+ /**
+ * Overrides the current shift progress to keep the app window at the bottom of the screen
+ * while the transient taskbar is being swiped in.
+ *
+ * There is also a catch up period so that the window can start moving 1:1 with the swipe.
+ */
+ private float getTaskbarProgress() {
+ if (!DisplayController.isTransientTaskbar(mContext)) {
+ return mCurrentShift.value;
+ }
+
+ if (mTaskbarAlreadyOpen) {
+ return mCurrentShift.value;
+ }
+
+ if (mCurrentDisplacement < mTaskbarAppWindowThreshold) {
+ return 0;
+ }
+
+ // "Catch up" with `mCurrentShift.value`.
+ if (mCurrentDisplacement < mTaskbarCatchUpThreshold) {
+ return Utilities.mapToRange(mCurrentDisplacement, mTaskbarAppWindowThreshold,
+ mTaskbarCatchUpThreshold, 0, mCurrentShift.value, ACCEL_DEACCEL);
+ }
+
+ return mCurrentShift.value;
+ }
+
private void setDividerShown(boolean shown, boolean immediate) {
if (mDividerAnimator != null) {
mDividerAnimator.cancel();
diff --git a/quickstep/src/com/android/quickstep/AnimatedFloat.java b/quickstep/src/com/android/quickstep/AnimatedFloat.java
index b06b894..5ab3c58 100644
--- a/quickstep/src/com/android/quickstep/AnimatedFloat.java
+++ b/quickstep/src/com/android/quickstep/AnimatedFloat.java
@@ -98,15 +98,6 @@
}
}
- /**
- * Starts the animation.
- */
- public void startAnimation() {
- if (mValueAnimator != null) {
- mValueAnimator.start();
- }
- }
-
public void cancelAnimation() {
if (mValueAnimator != null) {
mValueAnimator.cancel();
@@ -119,10 +110,6 @@
}
}
- public ObjectAnimator getCurrentAnimation() {
- return mValueAnimator;
- }
-
public boolean isAnimating() {
return mValueAnimator != null;
}
@@ -140,11 +127,4 @@
public boolean isSettledOnValue(float endValue) {
return !isAnimating() && value == endValue;
}
-
- /**
- * Returns the value we are animating to, or {@code null} if we are not currently animating.
- */
- public Float getEndValue() {
- return mEndValue;
- }
}
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index de150e1..a622326 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -30,6 +30,7 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Context;
@@ -501,45 +502,43 @@
"setRecentsAttachedToAppWindow: exiting early"));
return;
}
- mIsAttachedToWindow = attached;
- RecentsView recentsView = mActivity.getOverviewPanel();
- if (attached) {
- mHasEverAttachedToWindow = true;
- }
- Animator fadeAnim = mActivity.getStateManager()
- .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
-
- float fromTranslation = attached ? 1 : 0;
- float toTranslation = attached ? 0 : 1;
+ mActivity.getStateManager()
+ .cancelStateElementAnimation(INDEX_RECENTS_FADE_ANIM);
mActivity.getStateManager()
.cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
- if (!recentsView.isShown() && animate) {
- ADJACENT_PAGE_HORIZONTAL_OFFSET.set(recentsView, fromTranslation);
- ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
- "setRecentsAttachedToAppWindow: recents view not shown, setting ")
- .append("ADJACENT_PAGE_HORIZONTAL_OFFSET to ")
- .append(Float.toString(fromTranslation)));
- } else {
- fromTranslation = ADJACENT_PAGE_HORIZONTAL_OFFSET.get(recentsView);
- ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
- "setRecentsAttachedToAppWindow: updating fromTranslation to ")
- .append(Float.toString(fromTranslation)));
- }
+
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ mIsAttachedToWindow = attached;
+ if (attached) {
+ mHasEverAttachedToWindow = true;
+ }
+ }});
+
+ long animationDuration = animate ? RECENTS_ATTACH_DURATION : 0;
+ Animator fadeAnim = mActivity.getStateManager()
+ .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
+ fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
+ fadeAnim.setDuration(animationDuration);
+ animatorSet.play(fadeAnim);
+
+ float fromTranslation = ADJACENT_PAGE_HORIZONTAL_OFFSET.get(
+ mActivity.getOverviewPanel());
+ float toTranslation = attached ? 0 : 1;
ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
"setRecentsAttachedToAppWindow: fromTranslation=")
.append(Float.toString(fromTranslation))
.append(", toTranslation=")
.append(Float.toString(toTranslation)));
- if (!animate) {
- ADJACENT_PAGE_HORIZONTAL_OFFSET.set(recentsView, toTranslation);
- } else {
- mActivity.getStateManager().createStateElementAnimation(
- INDEX_RECENTS_TRANSLATE_X_ANIM,
- fromTranslation, toTranslation).start();
- }
- fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
- fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
+ Animator translationAnimator = mActivity.getStateManager().createStateElementAnimation(
+ INDEX_RECENTS_TRANSLATE_X_ANIM, fromTranslation, toTranslation);
+ translationAnimator.setDuration(animationDuration);
+ animatorSet.play(translationAnimator);
+ animatorSet.start();
}
@Override
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 3c4ee75..31b78b3 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -21,7 +21,6 @@
import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_HOME;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_LAST_TASK;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.SET_END_TARGET_NEW_TASK;
import android.annotation.Nullable;
@@ -341,8 +340,6 @@
ActiveGestureLog.INSTANCE.trackEvent(SET_END_TARGET_NEW_TASK);
break;
case LAST_TASK:
- ActiveGestureLog.INSTANCE.trackEvent(SET_END_TARGET_LAST_TASK);
- break;
case RECENTS:
default:
// No-Op
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index b7cdecd..9621ce6 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -20,6 +20,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
+import java.util.function.Function;
public class QuickstepTestInformationHandler extends TestInformationHandler {
@@ -112,6 +113,13 @@
resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size));
return response;
}
+
+ case TestProtocol.REQUEST_TASKBAR_ALL_APPS_TOP_PADDING: {
+ return getTISBinderUIProperty(Bundle::putInt, tisBinder ->
+ tisBinder.getTaskbarManager()
+ .getCurrentActivityContext()
+ .getTaskbarAllAppsTopPadding());
+ }
}
return super.call(method, arg, extras);
@@ -159,4 +167,16 @@
throw new RuntimeException(e);
}
}
+
+ private <T> Bundle getTISBinderUIProperty(
+ BundleSetter<T> bundleSetter, Function<TouchInteractionService.TISBinder, T> provider) {
+ Bundle response = new Bundle();
+
+ runOnTISBinder(tisBinder -> bundleSetter.set(
+ response,
+ TestProtocol.TEST_INFO_RESPONSE_FIELD,
+ provider.apply(tisBinder)));
+
+ return response;
+ }
}
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 7bcc661..d46565b 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -23,6 +23,8 @@
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.KeyguardManager;
+import android.app.TaskInfo;
+import android.content.ComponentName;
import android.os.Build;
import android.os.Process;
import android.os.RemoteException;
@@ -324,8 +326,14 @@
writer.println(prefix + " mChangeId=" + mChangeId);
writer.println(prefix + " mResultsUi=[id=" + mResultsUi.mRequestId + ", tasks=");
for (GroupTask task : mResultsUi) {
- writer.println(prefix + " t1=" + task.task1.key.id
- + " t2=" + (task.hasMultipleTasks() ? task.task2.key.id : "-1"));
+ Task task1 = task.task1;
+ Task task2 = task.task2;
+ ComponentName cn1 = task1.getTopComponent();
+ ComponentName cn2 = task2 != null ? task2.getTopComponent() : null;
+ writer.println(prefix + " t1: (id=" + task1.key.id
+ + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)")
+ + " t2: (id=" + (task2 != null ? task2.key.id : "-1")
+ + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" : "no package)"));
}
writer.println(prefix + " ]");
int currentUserId = Process.myUserHandle().getIdentifier();
@@ -333,8 +341,14 @@
mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
writer.println(prefix + " rawTasks=[");
for (GroupedRecentTaskInfo task : rawTasks) {
- writer.println(prefix + " t1=" + task.getTaskInfo1().taskId
- + " t2=" + (task.getTaskInfo2() != null ? task.getTaskInfo2().taskId : "-1"));
+ TaskInfo taskInfo1 = task.getTaskInfo1();
+ TaskInfo taskInfo2 = task.getTaskInfo2();
+ ComponentName cn1 = taskInfo1.topActivity;
+ ComponentName cn2 = taskInfo2 != null ? taskInfo2.topActivity : null;
+ writer.println(prefix + " t1: (id=" + taskInfo1.taskId
+ + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)")
+ + " t2: (id=" + (taskInfo2 != null ? taskInfo2.taskId : "-1")
+ + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" : "no package)"));
}
writer.println(prefix + " ]");
}
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 97ce30f..dc405ff 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -37,9 +37,11 @@
import android.os.Handler;
import android.os.Looper;
import android.view.Display;
+import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl.Transaction;
import android.view.View;
+import android.window.RemoteTransition;
import android.window.SplashScreen;
import androidx.annotation.Nullable;
@@ -79,7 +81,6 @@
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -259,13 +260,11 @@
final LauncherAnimationRunner wrapper = new LauncherAnimationRunner(
mUiHandler, mActivityLaunchAnimationRunner, true /* startAtFrontOfQueue */);
- RemoteAnimationAdapterCompat adapterCompat = new RemoteAnimationAdapterCompat(
- wrapper, RECENTS_LAUNCH_DURATION,
- RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION
- - STATUS_BAR_TRANSITION_PRE_DELAY, getIApplicationThread());
final ActivityOptions options = ActivityOptions.makeRemoteAnimation(
- adapterCompat.getWrapped(),
- adapterCompat.getRemoteTransition().getTransition());
+ new RemoteAnimationAdapter(wrapper, RECENTS_LAUNCH_DURATION,
+ RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION
+ - STATUS_BAR_TRANSITION_PRE_DELAY),
+ new RemoteTransition(wrapper.toRemoteTransition(), getIApplicationThread()));
final ActivityOptionsWrapper activityOptions = new ActivityOptionsWrapper(options,
onEndCallback);
activityOptions.options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
@@ -401,12 +400,9 @@
private void startHomeInternal() {
LauncherAnimationRunner runner = new LauncherAnimationRunner(
getMainThreadHandler(), mAnimationToHomeFactory, true);
- RemoteAnimationAdapterCompat adapterCompat =
- new RemoteAnimationAdapterCompat(runner, HOME_APPEAR_DURATION, 0,
- getIApplicationThread());
ActivityOptions options = ActivityOptions.makeRemoteAnimation(
- adapterCompat.getWrapped(),
- adapterCompat.getRemoteTransition().getTransition());
+ new RemoteAnimationAdapter(runner, HOME_APPEAR_DURATION, 0),
+ new RemoteTransition(runner.toRemoteTransition(), getIApplicationThread()));
startHomeIntentSafely(this, options.toBundle());
}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index 2451ad8..b82ff03 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -130,7 +130,7 @@
public final void onAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) {
Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
ActiveGestureLog.INSTANCE.addLog(
- /* event= */ "onRecentsAnimationCancelled",
+ /* event= */ "RecentsAnimationCallbacks.onAnimationCanceled",
/* gestureEvent= */ CANCEL_RECENTS_ANIMATION);
for (RecentsAnimationListener listener : getListeners()) {
listener.onRecentsAnimationCanceled(thumbnailDatas);
@@ -142,7 +142,7 @@
@Override
public void onTasksAppeared(RemoteAnimationTarget[] apps) {
Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
- ActiveGestureLog.INSTANCE.addLog("onTasksAppeared",
+ ActiveGestureLog.INSTANCE.addLog("RecentsAnimationCallbacks.onTasksAppeared",
ActiveGestureErrorDetector.GestureEvent.TASK_APPEARED);
for (RecentsAnimationListener listener : getListeners()) {
listener.onTasksAppeared(apps);
@@ -164,6 +164,8 @@
private final void onAnimationFinished(RecentsAnimationController controller) {
Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
+ ActiveGestureLog.INSTANCE.addLog(
+ /* event= */ "RecentsAnimationCallbacks.onAnimationFinished");
for (RecentsAnimationListener listener : getListeners()) {
listener.onRecentsAnimationFinished(controller);
}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 6088a36..4adfae5 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FINISH_RECENTS_ANIMATION;
import android.content.Context;
import android.os.RemoteException;
@@ -155,6 +156,10 @@
mPendingFinishCallbacks.add(callback);
return;
}
+ ActiveGestureLog.INSTANCE.addLog(
+ /* event= */ "finishRecentsAnimation",
+ /* extras= */ toRecents,
+ /* gestureEvent= */ FINISH_RECENTS_ANIMATION);
// Finish not yet requested
mFinishRequested = true;
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index f591a1c..ddb06ce 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -65,6 +65,7 @@
// 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
// visible.
protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
+ protected float mCurrentDisplacement;
// The distance needed to drag to reach the task size in recents.
protected int mTransitionDragLength;
@@ -116,6 +117,8 @@
public void updateDisplacement(float displacement) {
// We are moving in the negative x/y direction
displacement = -displacement;
+ mCurrentDisplacement = displacement;
+
float shift;
if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
shift = mDragLengthFactor;
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 7705a25..acf597b 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -44,6 +44,8 @@
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.IOnBackInvokedCallback;
+import android.window.RemoteTransition;
+import android.window.TransitionFilter;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
@@ -53,13 +55,11 @@
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.systemui.shared.recents.ISystemUiProxy;
import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.system.RemoteTransitionCompat;
import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController;
import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
import com.android.systemui.shared.system.smartspace.SmartspaceState;
import com.android.wm.shell.back.IBackAnimation;
import com.android.wm.shell.desktopmode.IDesktopMode;
-import com.android.wm.shell.floating.IFloatingTasks;
import com.android.wm.shell.onehanded.IOneHanded;
import com.android.wm.shell.pip.IPip;
import com.android.wm.shell.pip.IPipAnimationListener;
@@ -74,6 +74,7 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.LinkedHashMap;
/**
* Holds the reference to SystemUI.
@@ -90,7 +91,6 @@
private IPip mPip;
private ISysuiUnlockAnimationController mSysuiUnlockAnimationController;
private ISplitScreen mSplitScreen;
- private IFloatingTasks mFloatingTasks;
private IOneHanded mOneHanded;
private IShellTransitions mShellTransitions;
private IStartingWindow mStartingWindow;
@@ -110,7 +110,8 @@
private IStartingWindowListener mStartingWindowListener;
private ILauncherUnlockAnimationController mLauncherUnlockAnimationController;
private IRecentTasksListener mRecentTasksListener;
- private final ArrayList<RemoteTransitionCompat> mRemoteTransitions = new ArrayList<>();
+ private final LinkedHashMap<RemoteTransition, TransitionFilter> mRemoteTransitions =
+ new LinkedHashMap<>();
private IOnBackInvokedCallback mBackToLauncherCallback;
// Used to dedupe calls to SystemUI
@@ -168,7 +169,7 @@
}
public void setProxy(ISystemUiProxy proxy, IPip pip, ISplitScreen splitScreen,
- IFloatingTasks floatingTasks, IOneHanded oneHanded, IShellTransitions shellTransitions,
+ IOneHanded oneHanded, IShellTransitions shellTransitions,
IStartingWindow startingWindow, IRecentTasks recentTasks,
ISysuiUnlockAnimationController sysuiUnlockAnimationController,
IBackAnimation backAnimation, IDesktopMode desktopMode) {
@@ -176,7 +177,6 @@
mSystemUiProxy = proxy;
mPip = pip;
mSplitScreen = splitScreen;
- mFloatingTasks = floatingTasks;
mOneHanded = oneHanded;
mShellTransitions = shellTransitions;
mStartingWindow = startingWindow;
@@ -198,9 +198,7 @@
if (mSysuiUnlockAnimationController != null && mLauncherUnlockAnimationController != null) {
setLauncherUnlockAnimationController(mLauncherUnlockAnimationController);
}
- for (int i = mRemoteTransitions.size() - 1; i >= 0; --i) {
- registerRemoteTransition(mRemoteTransitions.get(i));
- }
+ new LinkedHashMap<>(mRemoteTransitions).forEach(this::registerRemoteTransition);
if (mRecentTasksListener != null && mRecentTasks != null) {
registerRecentTasksListener(mRecentTasksListener);
}
@@ -210,7 +208,7 @@
}
public void clearProxy() {
- setProxy(null, null, null, null, null, null, null, null, null, null, null);
+ setProxy(null, null, null, null, null, null, null, null, null, null);
}
// TODO(141886704): Find a way to remove this
@@ -347,17 +345,6 @@
}
@Override
- public void notifySwipeUpGestureStarted() {
- if (mSystemUiProxy != null) {
- try {
- mSystemUiProxy.notifySwipeUpGestureStarted();
- } catch (RemoteException e) {
- Log.w(TAG, "Failed call notifySwipeUpGestureStarted", e);
- }
- }
- }
-
- @Override
public void notifyPrioritizedRotation(int rotation) {
if (mSystemUiProxy != null) {
try {
@@ -550,11 +537,11 @@
/** Start multiple tasks in split-screen simultaneously. */
public void startTasks(int taskId1, Bundle options1, int taskId2, Bundle options2,
@SplitConfigurationOptions.StagePosition int splitPosition, float splitRatio,
- RemoteTransitionCompat remoteTransition, InstanceId instanceId) {
+ RemoteTransition remoteTransition, InstanceId instanceId) {
if (mSystemUiProxy != null) {
try {
mSplitScreen.startTasks(taskId1, options1, taskId2, options2, splitPosition,
- splitRatio, remoteTransition.getTransition(), instanceId);
+ splitRatio, remoteTransition, instanceId);
} catch (RemoteException e) {
Log.w(TAG, "Failed call startTasks");
}
@@ -564,12 +551,12 @@
public void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent,
Bundle options1, int taskId, Bundle options2,
@SplitConfigurationOptions.StagePosition int splitPosition, float splitRatio,
- RemoteTransitionCompat remoteTransition, InstanceId instanceId) {
+ RemoteTransition remoteTransition, InstanceId instanceId) {
if (mSystemUiProxy != null) {
try {
mSplitScreen.startIntentAndTask(pendingIntent, fillInIntent, options1,
taskId, options2, splitPosition, splitRatio,
- remoteTransition.getTransition(), instanceId);
+ remoteTransition, instanceId);
} catch (RemoteException e) {
Log.w(TAG, "Failed call startIntentAndTask");
}
@@ -578,11 +565,11 @@
public void startShortcutAndTask(ShortcutInfo shortcutInfo, Bundle options1, int taskId,
Bundle options2, @SplitConfigurationOptions.StagePosition int splitPosition,
- float splitRatio, RemoteTransitionCompat remoteTransition, InstanceId instanceId) {
+ float splitRatio, RemoteTransition remoteTransition, InstanceId instanceId) {
if (mSystemUiProxy != null) {
try {
mSplitScreen.startShortcutAndTask(shortcutInfo, options1, taskId, options2,
- splitPosition, splitRatio, remoteTransition.getTransition(), instanceId);
+ splitPosition, splitRatio, remoteTransition, instanceId);
} catch (RemoteException e) {
Log.w(TAG, "Failed call startShortcutAndTask");
}
@@ -696,20 +683,6 @@
}
//
- // Floating tasks
- //
-
- public void showFloatingTask(Intent intent) {
- if (mFloatingTasks != null) {
- try {
- mFloatingTasks.showTask(intent);
- } catch (RemoteException e) {
- Log.w(TAG, "Launcher: Failed call showFloatingTask", e);
- }
- }
- }
-
- //
// One handed
//
@@ -737,24 +710,24 @@
// Remote transitions
//
- public void registerRemoteTransition(RemoteTransitionCompat remoteTransition) {
+ public void registerRemoteTransition(
+ RemoteTransition remoteTransition, TransitionFilter filter) {
if (mShellTransitions != null) {
try {
- mShellTransitions.registerRemote(remoteTransition.getFilter(),
- remoteTransition.getTransition());
+ mShellTransitions.registerRemote(filter, remoteTransition);
} catch (RemoteException e) {
Log.w(TAG, "Failed call registerRemoteTransition");
}
}
- if (!mRemoteTransitions.contains(remoteTransition)) {
- mRemoteTransitions.add(remoteTransition);
+ if (!mRemoteTransitions.containsKey(remoteTransition)) {
+ mRemoteTransitions.put(remoteTransition, filter);
}
}
- public void unregisterRemoteTransition(RemoteTransitionCompat remoteTransition) {
+ public void unregisterRemoteTransition(RemoteTransition remoteTransition) {
if (mShellTransitions != null) {
try {
- mShellTransitions.unregisterRemote(remoteTransition.getTransition());
+ mShellTransitions.unregisterRemote(remoteTransition);
} catch (RemoteException e) {
Log.w(TAG, "Failed call registerRemoteTransition");
}
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 30d445f..c45b2f0 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -21,6 +21,8 @@
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED;
import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION;
+import static com.android.systemui.shared.system.RemoteTransitionCompat.newRemoteTransition;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -29,6 +31,7 @@
import android.os.SystemProperties;
import android.util.Log;
import android.view.RemoteAnimationTarget;
+import android.window.RemoteTransition;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
@@ -36,10 +39,10 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
+import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.RemoteTransitionCompat;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -100,6 +103,9 @@
@UiThread
public RecentsAnimationCallbacks startRecentsAnimation(GestureState gestureState,
Intent intent, RecentsAnimationCallbacks.RecentsAnimationListener listener) {
+ ActiveGestureLog.INSTANCE.addLog(
+ /* event= */ "startRecentsAnimation",
+ /* gestureEvent= */ START_RECENTS_ANIMATION);
// Notify if recents animation is still running
if (mController != null) {
String msg = "New recents animation started before old animation completed";
@@ -223,11 +229,10 @@
mCallbacks.addListener(listener);
if (ENABLE_SHELL_TRANSITIONS) {
- RemoteTransitionCompat transition = new RemoteTransitionCompat(mCallbacks,
+ RemoteTransition transition = newRemoteTransition(mCallbacks,
mController != null ? mController.getController() : null,
mCtx.getIApplicationThread());
- final ActivityOptions options = ActivityOptions.makeRemoteTransition(
- transition.getTransition());
+ final ActivityOptions options = ActivityOptions.makeRemoteTransition(transition);
// Allowing to pause Home if Home is top activity and Recents is not Home. So when user
// start home when recents animation is playing, the home activity can be resumed again
// to let the transition controller collect Home activity.
@@ -250,6 +255,7 @@
* Continues the existing running recents animation for a new gesture.
*/
public RecentsAnimationCallbacks continueRecentsAnimation(GestureState gestureState) {
+ ActiveGestureLog.INSTANCE.addLog(/* event= */ "continueRecentsAnimation");
mCallbacks.removeListener(mLastGestureState);
mLastGestureState = gestureState;
mCallbacks.addListener(gestureState);
@@ -288,6 +294,8 @@
*/
public void finishRunningRecentsAnimation(boolean toHome) {
if (mController != null) {
+ ActiveGestureLog.INSTANCE.addLog(
+ /* event= */ "finishRunningRecentsAnimation", toHome);
mCallbacks.notifyAnimationCanceled();
Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome
? mController::finishAnimationToHome
@@ -320,6 +328,7 @@
* Cleans up the recents animation entirely.
*/
private void cleanUpRecentsAnimation() {
+ ActiveGestureLog.INSTANCE.addLog(/* event= */ "cleanUpRecentsAnimation");
if (mLiveTileCleanUpHandler != null) {
mLiveTileCleanUpHandler.run();
mLiveTileCleanUpHandler = null;
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 9d5e7c3..e8722e2 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -43,7 +43,6 @@
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
-import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Matrix;
@@ -399,9 +398,8 @@
*/
public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView,
@NonNull StateManager stateManager, @Nullable DepthController depthController,
- int initialTaskId, @Nullable PendingIntent initialTaskPendingIntent, int secondTaskId,
- @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t,
- @NonNull Runnable finishCallback) {
+ int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo,
+ SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
if (launchingTaskView != null) {
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.addListener(new AnimatorListenerAdapter() {
@@ -491,8 +489,7 @@
* If it is null, then it will simply fade in the starting apps and fade out launcher (for the
* case where launcher handles animating starting split tasks from app icon) */
public static void composeRecentsSplitLaunchAnimatorLegacy(
- @Nullable GroupedTaskView launchingTaskView, int initialTaskId,
- @Nullable PendingIntent initialTaskPendingIntent, int secondTaskId,
+ @Nullable GroupedTaskView launchingTaskView, int initialTaskId, int secondTaskId,
@NonNull RemoteAnimationTarget[] appTargets,
@NonNull RemoteAnimationTarget[] wallpaperTargets,
@NonNull RemoteAnimationTarget[] nonAppTargets,
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index e9f9d80..450774b 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -15,6 +15,7 @@
*/
package com.android.quickstep;
+import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
@@ -22,6 +23,7 @@
import static com.android.launcher3.config.FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.quickstep.GestureState.DEFAULT_STATE;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_UP;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
@@ -32,7 +34,6 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
-import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_FLOATING_TASKS;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
@@ -47,7 +48,6 @@
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
-import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.Icon;
import android.os.Build;
@@ -55,11 +55,11 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.SystemClock;
-import android.os.SystemProperties;
import android.util.Log;
import android.view.Choreographer;
import android.view.InputEvent;
import android.view.MotionEvent;
+import android.view.SurfaceControl;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.BinderThread;
@@ -86,7 +86,6 @@
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.OnboardingPrefs;
import com.android.launcher3.util.TraceHelper;
-import com.android.launcher3.util.WindowBounds;
import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
import com.android.quickstep.inputconsumers.AssistantInputConsumer;
import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
@@ -103,7 +102,6 @@
import com.android.quickstep.util.ActiveGestureLog.CompoundString;
import com.android.quickstep.util.ProtoTracer;
import com.android.quickstep.util.ProxyScreenStatusProvider;
-import com.android.quickstep.util.SplitScreenBounds;
import com.android.systemui.shared.recents.IOverviewProxy;
import com.android.systemui.shared.recents.ISystemUiProxy;
import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -114,7 +112,6 @@
import com.android.systemui.shared.tracing.ProtoTraceable;
import com.android.wm.shell.back.IBackAnimation;
import com.android.wm.shell.desktopmode.IDesktopMode;
-import com.android.wm.shell.floating.IFloatingTasks;
import com.android.wm.shell.onehanded.IOneHanded;
import com.android.wm.shell.pip.IPip;
import com.android.wm.shell.recents.IRecentTasks;
@@ -140,21 +137,7 @@
private static final String TAG = "TouchInteractionService";
- private static final boolean BUBBLES_HOME_GESTURE_ENABLED =
- SystemProperties.getBoolean("persist.wm.debug.bubbles_home_gesture", true);
-
- private static final String KEY_BACK_NOTIFICATION_COUNT = "backNotificationCount";
- private static final String NOTIFY_ACTION_BACK = "com.android.quickstep.action.BACK_GESTURE";
private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once";
- private static final int MAX_BACK_NOTIFICATION_COUNT = 3;
-
- /**
- * System Action ID to show all apps.
- * TODO: Use AccessibilityService's corresponding global action constant in S
- */
- private static final int SYSTEM_ACTION_ID_ALL_APPS = 14;
-
- private int mBackGestureNotificationCounter = -1;
private final TISBinder mTISBinder = new TISBinder();
@@ -172,8 +155,6 @@
IPip pip = IPip.Stub.asInterface(bundle.getBinder(KEY_EXTRA_SHELL_PIP));
ISplitScreen splitscreen = ISplitScreen.Stub.asInterface(bundle.getBinder(
KEY_EXTRA_SHELL_SPLIT_SCREEN));
- IFloatingTasks floatingTasks = IFloatingTasks.Stub.asInterface(bundle.getBinder(
- KEY_EXTRA_SHELL_FLOATING_TASKS));
IOneHanded onehanded = IOneHanded.Stub.asInterface(
bundle.getBinder(KEY_EXTRA_SHELL_ONE_HANDED));
IShellTransitions shellTransitions = IShellTransitions.Stub.asInterface(
@@ -191,7 +172,7 @@
bundle.getBinder(KEY_EXTRA_SHELL_DESKTOP_MODE));
MAIN_EXECUTOR.execute(() -> {
SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy, pip,
- splitscreen, floatingTasks, onehanded, shellTransitions, startingWindow,
+ splitscreen, onehanded, shellTransitions, startingWindow,
recentTasks, launcherUnlockAnimationController, backAnimation, desktopMode);
TouchInteractionService.this.initInputMonitor("TISBinder#onInitialize()");
preloadOverview(true /* fromInit */);
@@ -232,12 +213,6 @@
@BinderThread
@Override
- public void onTip(int actionType, int viewType) {
- // Please delete this method from the interface
- }
-
- @BinderThread
- @Override
public void onAssistantAvailable(boolean available) {
MAIN_EXECUTOR.execute(() -> {
mDeviceState.setAssistantAvailable(available);
@@ -254,10 +229,9 @@
});
}
- @BinderThread
- public void onBackAction(boolean completed, int downX, int downY, boolean isButton,
- boolean gestureSwipeLeft) {
- // Remove this method from the interface
+ @Override
+ public void onNavigationBarSurface(SurfaceControl surface) {
+ // TODO: implement
}
@BinderThread
@@ -274,12 +248,6 @@
MAIN_EXECUTOR.execute(() -> mDeviceState.setDeferredGestureRegion(region));
}
- @Override
- public void onSplitScreenSecondaryBoundsChanged(Rect bounds, Rect insets) {
- WindowBounds wb = new WindowBounds(bounds, insets);
- MAIN_EXECUTOR.execute(() -> SplitScreenBounds.INSTANCE.setSecondaryWindowBounds(wb));
- }
-
@BinderThread
@Override
public void onScreenTurnedOn() {
@@ -298,6 +266,16 @@
MAIN_EXECUTOR.execute(ProxyScreenStatusProvider.INSTANCE::onScreenTurningOff);
}
+ @BinderThread
+ @Override
+ public void enterStageSplitFromRunningApp(boolean leftOrTop) {
+ StatefulActivity activity =
+ mOverviewComponentObserver.getActivityInterface().getCreatedActivity();
+ if (activity != null) {
+ activity.enterStageSplitFromRunningApp(leftOrTop);
+ }
+ }
+
/**
* Preloads the Overview activity.
*
@@ -483,8 +461,6 @@
// Temporarily disable model preload
// new ModelPreload().start(this);
- mBackGestureNotificationCounter = Math.max(0, Utilities.getDevicePrefs(this)
- .getInt(KEY_BACK_NOTIFICATION_COUNT, MAX_BACK_NOTIFICATION_COUNT));
resetHomeBounceSeenOnQuickstepEnabledFirstTime();
mOverviewComponentObserver.setOverviewChangeListener(this::onOverviewTargetChange);
@@ -522,11 +498,11 @@
Icon.createWithResource(this, R.drawable.ic_apps),
getString(R.string.all_apps_label),
getString(R.string.all_apps_label),
- PendingIntent.getActivity(this, SYSTEM_ACTION_ID_ALL_APPS, intent,
+ PendingIntent.getActivity(this, GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE));
- am.registerSystemAction(allAppsAction, SYSTEM_ACTION_ID_ALL_APPS);
+ am.registerSystemAction(allAppsAction, GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
} else {
- am.unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
+ am.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
}
StatefulActivity newOverviewActivity = mOverviewComponentObserver.getActivityInterface()
@@ -604,7 +580,7 @@
ProtoTracer.INSTANCE.get(this).remove(this);
getSystemService(AccessibilityManager.class)
- .unregisterSystemAction(SYSTEM_ACTION_ID_ALL_APPS);
+ .unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
mTaskbarManager.destroy();
sConnected = false;
@@ -829,27 +805,9 @@
if (mDeviceState.isBubblesExpanded()) {
reasonString = newCompoundString(reasonPrefix)
.append(SUBSTRING_PREFIX)
- .append("bubbles expanded");
- if (BUBBLES_HOME_GESTURE_ENABLED) {
- reasonString.append(SUBSTRING_PREFIX)
- .append("bubbles can handle the home gesture")
- .append(", trying to use default input consumer");
- // Bubbles can handle home gesture itself.
- base = getDefaultInputConsumer(reasonString);
- } else {
- // If Bubbles is expanded, use the overlay input consumer, which will close
- // Bubbles instead of going all the way home when a swipe up is detected.
- // Notification panel can be expanded on top of expanded bubbles. Bubbles remain
- // expanded in the back. Make sure swipe up is not passed to bubbles in this
- // case.
- if (!mDeviceState.isNotificationPanelExpanded()) {
- reasonString = newCompoundString(reasonPrefix)
- .append(SUBSTRING_PREFIX)
- .append("using SysUiOverlayInputConsumer");
- base = new SysUiOverlayInputConsumer(
- getBaseContext(), mDeviceState, mInputMonitorCompat);
- }
- }
+ .append("bubbles expanded, trying to use default input consumer");
+ // Bubbles can handle home gesture itself.
+ base = getDefaultInputConsumer(reasonString);
}
if (mDeviceState.isSystemUiDialogShowing()) {
@@ -927,6 +885,9 @@
.append(consumer.getName())
.append(". reason(s):")
.append(reasonString));
+ if ((consumer.getType() & InputConsumer.TYPE_OTHER_ACTIVITY) != 0) {
+ ActiveGestureLog.INSTANCE.trackEvent(FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER);
+ }
}
private void handleOrientationSetup(InputConsumer baseInputConsumer) {
@@ -1251,12 +1212,22 @@
}
private void onCommand(PrintWriter pw, LinkedList<String> args) {
- switch (args.pollFirst()) {
+ String cmd = args.pollFirst();
+ if (cmd == null) {
+ pw.println("Command missing");
+ printAvailableCommands(pw);
+ return;
+ }
+ switch (cmd) {
case "clear-touch-log":
ActiveGestureLog.INSTANCE.clear();
break;
case "print-gesture-log":
ActiveGestureLog.INSTANCE.dump("", pw);
+ break;
+ default:
+ pw.println("Command does not exist: " + cmd);
+ printAvailableCommands(pw);
}
}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 60d5ba4..b3d3c3d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -30,13 +30,13 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
import static com.android.launcher3.util.VelocityUtils.PX_PER_MS;
-import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION;
import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
+import android.content.res.Resources;
import android.graphics.PointF;
import android.os.Build;
import android.util.Log;
@@ -48,13 +48,16 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.tracing.InputConsumerProto;
+import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.TraceHelper;
import com.android.quickstep.AbsSwipeUpHandler;
import com.android.quickstep.AbsSwipeUpHandler.Factory;
+import com.android.quickstep.BaseActivityInterface;
import com.android.quickstep.GestureState;
import com.android.quickstep.InputConsumer;
import com.android.quickstep.RecentsAnimationCallbacks;
@@ -63,7 +66,6 @@
import com.android.quickstep.RecentsAnimationTargets;
import com.android.quickstep.RotationTouchHelper;
import com.android.quickstep.TaskAnimationManager;
-import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.CachedEventDispatcher;
import com.android.quickstep.util.MotionPauseDetector;
import com.android.quickstep.util.NavBarPosition;
@@ -97,6 +99,7 @@
private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
private final InputMonitorCompat mInputMonitorCompat;
private final InputEventReceiver mInputEventReceiver;
+ private final BaseActivityInterface mActivityInterface;
private final AbsSwipeUpHandler.Factory mHandlerFactory;
@@ -131,6 +134,10 @@
// Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
private float mStartDisplacement;
+ private final boolean mIsTransientTaskbar;
+ private final boolean mTaskbarAlreadyOpen;
+ private final int mTaskbarHomeOverviewThreshold;
+
public OtherActivityInputConsumer(Context base, RecentsAnimationDeviceState deviceState,
TaskAnimationManager taskAnimationManager, GestureState gestureState,
boolean isDeferredDownTarget, Consumer<OtherActivityInputConsumer> onCompleteCallback,
@@ -142,6 +149,11 @@
mTaskAnimationManager = taskAnimationManager;
mGestureState = gestureState;
mHandlerFactory = handlerFactory;
+ mActivityInterface = mGestureState.getActivityInterface();
+
+ Resources res = base.getResources();
+ mTaskbarHomeOverviewThreshold = res
+ .getDimensionPixelSize(R.dimen.taskbar_home_overview_threshold);
mMotionPauseDetector = new MotionPauseDetector(base, false,
mNavBarPosition.isLeftEdge() || mNavBarPosition.isRightEdge()
@@ -153,6 +165,10 @@
mInputMonitorCompat = inputMonitorCompat;
mInputEventReceiver = inputEventReceiver;
+ TaskbarUIController controller = mActivityInterface.getTaskbarController();
+ mTaskbarAlreadyOpen = controller != null && !controller.isTaskbarStashed();
+ mIsTransientTaskbar = DisplayController.isTransientTaskbar(base);
+
boolean continuingPreviousGesture = mTaskAnimationManager.isRecentsAnimationRunning();
mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
@@ -279,6 +295,7 @@
float upDist = -displacement;
boolean passedSlop = squaredHypot(displacementX, displacementY)
>= mSquaredTouchSlop;
+
if (!mPassedSlopOnThisGesture && passedSlop) {
mPassedSlopOnThisGesture = true;
}
@@ -323,7 +340,13 @@
}
if (mDeviceState.isFullyGesturalNavMode()) {
- mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
+ float minDisplacement = mMotionPauseMinDisplacement;
+
+ if (mIsTransientTaskbar && !mTaskbarAlreadyOpen) {
+ minDisplacement += mTaskbarHomeOverviewThreshold;
+ }
+
+ mMotionPauseDetector.setDisallowPause(upDist < minDisplacement
|| isLikelyToStartNewTask);
mMotionPauseDetector.addPosition(ev);
mInteractionHandler.setIsLikelyToStartNewTask(isLikelyToStartNewTask);
@@ -357,13 +380,11 @@
// Notify the handler that the gesture has actually started
mInteractionHandler.onGestureStarted(isLikelyToStartNewTask);
+
+ mInteractionHandler.setTaskbarAlreadyOpen(mTaskbarAlreadyOpen);
}
private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
- ActiveGestureLog.INSTANCE.addLog(
- /* event= */ "startRecentsAnimation",
- /* gestureEvent= */ START_RECENTS_ANIMATION);
-
mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs);
mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler.getMotionPauseListener());
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
index b9b5e7c..2462394 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/ProgressDelegateInputConsumer.java
@@ -28,8 +28,6 @@
import android.graphics.Point;
import android.view.MotionEvent;
-import androidx.annotation.Nullable;
-
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
@@ -43,7 +41,6 @@
import com.android.quickstep.RecentsAnimationController;
import com.android.quickstep.RecentsAnimationTargets;
import com.android.quickstep.TaskAnimationManager;
-import com.android.quickstep.util.ActiveGestureErrorDetector;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.InputMonitorCompat;
@@ -102,8 +99,7 @@
mDisplaySize = DisplayController.INSTANCE.get(context).getInfo().currentSize;
// Init states
- mStateCallback = new MultiStateCallback(
- STATE_NAMES, ProgressDelegateInputConsumer::getTrackedEventForState);
+ mStateCallback = new MultiStateCallback(STATE_NAMES);
mStateCallback.runOnceAtState(STATE_TARGET_RECEIVED | STATE_HANDLER_INVALIDATED,
this::endRemoteAnimation);
mStateCallback.runOnceAtState(STATE_TARGET_RECEIVED | STATE_FLING_FINISHED,
@@ -113,14 +109,6 @@
mSwipeDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
}
- @Nullable
- private static ActiveGestureErrorDetector.GestureEvent getTrackedEventForState(int stateFlag) {
- if (stateFlag == STATE_HANDLER_INVALIDATED) {
- return ActiveGestureErrorDetector.GestureEvent.STATE_HANDLER_INVALIDATED;
- }
- return null;
- }
-
@Override
public int getType() {
return TYPE_PROGRESS_DELEGATE;
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
index 3785de4..b7f2022 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
@@ -15,16 +15,26 @@
*/
package com.android.quickstep.inputconsumers;
+import static android.view.MotionEvent.INVALID_POINTER_ID;
+
import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING;
import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PointF;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
+import com.android.launcher3.touch.OverScroll;
+import com.android.launcher3.util.DisplayController;
import com.android.quickstep.InputConsumer;
import com.android.systemui.shared.system.InputMonitorCompat;
@@ -37,20 +47,36 @@
private final GestureDetector mLongPressDetector;
private final float mSquaredTouchSlop;
-
- private float mDownX, mDownY;
+ private float mLongPressDownX, mLongPressDownY;
private boolean mCanceledUnstashHint;
private final float mUnstashArea;
private final float mScreenWidth;
+ private final int mTaskbarThresholdY;
+ private boolean mHasPassedTaskbarThreshold;
+
+ private final PointF mDownPos = new PointF();
+ private final PointF mLastPos = new PointF();
+ private int mActivePointerId = INVALID_POINTER_ID;
+
+ private final boolean mIsTransientTaskbar;
+
+ private final @Nullable TransitionCallback mTransitionCallback;
+
public TaskbarStashInputConsumer(Context context, InputConsumer delegate,
InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext) {
super(delegate, inputMonitor);
mTaskbarActivityContext = taskbarActivityContext;
mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
mScreenWidth = taskbarActivityContext.getDeviceProfile().widthPx;
- mUnstashArea = context.getResources()
- .getDimensionPixelSize(R.dimen.taskbar_unstash_input_area);
+
+ Resources res = context.getResources();
+ mUnstashArea = res.getDimensionPixelSize(R.dimen.taskbar_unstash_input_area);
+ int taskbarThreshold = res.getDimensionPixelSize(R.dimen.taskbar_nav_threshold);
+ int screenHeight = taskbarActivityContext.getDeviceProfile().heightPx;
+ mTaskbarThresholdY = screenHeight - taskbarThreshold;
+
+ mIsTransientTaskbar = DisplayController.isTransientTaskbar(context);
mLongPressDetector = new GestureDetector(context, new SimpleOnGestureListener() {
@Override
@@ -58,6 +84,10 @@
onLongPressDetected(motionEvent);
}
});
+
+ mTransitionCallback = mIsTransientTaskbar
+ ? taskbarActivityContext.getTranslationCallbacks()
+ : null;
}
@Override
@@ -76,28 +106,83 @@
final float y = ev.getRawY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
+ mActivePointerId = ev.getPointerId(0);
+ mDownPos.set(ev.getX(), ev.getY());
+ mLastPos.set(mDownPos);
+
+ mHasPassedTaskbarThreshold = false;
+ mTaskbarActivityContext.setAutohideSuspendFlag(
+ FLAG_AUTOHIDE_SUSPEND_TOUCHING, true);
if (isInArea(x)) {
- mDownX = x;
- mDownY = y;
- mTaskbarActivityContext.startTaskbarUnstashHint(
- /* animateForward = */ true);
- mCanceledUnstashHint = false;
+ if (!mIsTransientTaskbar) {
+ mLongPressDownX = x;
+ mLongPressDownY = y;
+ mTaskbarActivityContext.startTaskbarUnstashHint(
+ /* animateForward = */ true);
+ mCanceledUnstashHint = false;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ int ptrIdx = ev.getActionIndex();
+ int ptrId = ev.getPointerId(ptrIdx);
+ if (ptrId == mActivePointerId) {
+ final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
+ mDownPos.set(
+ ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
+ ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
+ mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
+ mActivePointerId = ev.getPointerId(newPointerIdx);
}
break;
case MotionEvent.ACTION_MOVE:
- if (!mCanceledUnstashHint
- && squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop) {
+ if (!mIsTransientTaskbar
+ && !mCanceledUnstashHint
+ && squaredHypot(mLongPressDownX - x, mLongPressDownY - y)
+ > mSquaredTouchSlop) {
mTaskbarActivityContext.startTaskbarUnstashHint(
/* animateForward = */ false);
mCanceledUnstashHint = true;
}
+
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == INVALID_POINTER_ID) {
+ break;
+ }
+ mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+ if (mIsTransientTaskbar) {
+ float dY = mLastPos.y - mDownPos.y;
+ boolean passedTaskbarThreshold = dY < 0
+ && mLastPos.y < mTaskbarThresholdY;
+
+ if (!mHasPassedTaskbarThreshold
+ && passedTaskbarThreshold) {
+ mHasPassedTaskbarThreshold = true;
+
+ mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+ }
+
+ if (dY < 0) {
+ dY = -OverScroll.dampedScroll(-dY, mTaskbarThresholdY);
+ if (mTransitionCallback != null) {
+ mTransitionCallback.onActionMove(dY);
+ }
+ }
+ }
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
- if (!mCanceledUnstashHint) {
+ if (!mIsTransientTaskbar && !mCanceledUnstashHint) {
mTaskbarActivityContext.startTaskbarUnstashHint(
/* animateForward = */ false);
}
+ mTaskbarActivityContext.setAutohideSuspendFlag(
+ FLAG_AUTOHIDE_SUSPEND_TOUCHING, false);
+ if (mTransitionCallback != null) {
+ mTransitionCallback.onActionEnd();
+ }
+ mHasPassedTaskbarThreshold = false;
break;
}
}
@@ -111,7 +196,9 @@
}
private void onLongPressDetected(MotionEvent motionEvent) {
- if (mTaskbarActivityContext != null && isInArea(motionEvent.getRawX())) {
+ if (mTaskbarActivityContext != null
+ && isInArea(motionEvent.getRawX())
+ && !mIsTransientTaskbar) {
boolean taskBarPressed = mTaskbarActivityContext.onLongPressToUnstashTaskbar();
if (taskBarPressed) {
setActive(motionEvent);
diff --git a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
index 7ff576e..8986c05 100644
--- a/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/AllSetActivity.java
@@ -135,7 +135,6 @@
} catch (URISyntaxException e) {
Log.e(LOG_TAG, "Failed to parse system nav settings intent", e);
}
- finish();
});
findViewById(R.id.hint).setAccessibilityDelegate(new SkipButtonAccessibilityDelegate());
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 791f93b..dac5a31 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -473,6 +473,7 @@
private int mPackageId = 0;
private long mLatencyInMillis;
private int mQueryLength = -1;
+ private int mSubEventType = 0;
StatsCompatLatencyLogger(Context context, ActivityContext activityContext) {
mContext = context;
@@ -510,6 +511,12 @@
}
@Override
+ public StatsLatencyLogger withSubEventType(int type) {
+ this.mSubEventType = type;
+ return this;
+ }
+
+ @Override
public void log(EventEnum event) {
if (IS_VERBOSE) {
String name = (event instanceof Enum) ? ((Enum) event).name() :
@@ -526,7 +533,8 @@
mPackageId, // package_id
mLatencyInMillis, // latency_in_millis
mType.getId(), //type
- mQueryLength // query_length
+ mQueryLength, // query_length
+ mSubEventType // sub_event_type
);
}
}
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
index 53e0c2b..60065fb 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -33,10 +33,11 @@
* Enums associated to gesture navigation events.
*/
public enum GestureEvent {
- MOTION_DOWN, MOTION_UP, SET_END_TARGET, SET_END_TARGET_HOME, SET_END_TARGET_LAST_TASK,
- SET_END_TARGET_NEW_TASK, ON_SETTLED_ON_END_TARGET, START_RECENTS_ANIMATION,
- FINISH_RECENTS_ANIMATION, CANCEL_RECENTS_ANIMATION, SET_ON_PAGE_TRANSITION_END_CALLBACK,
- CANCEL_CURRENT_ANIMATION, CLEANUP_SCREENSHOT, SCROLLER_ANIMATION_ABORTED, TASK_APPEARED,
+ MOTION_DOWN, MOTION_UP, SET_END_TARGET, SET_END_TARGET_HOME, SET_END_TARGET_NEW_TASK,
+ ON_SETTLED_ON_END_TARGET, START_RECENTS_ANIMATION, FINISH_RECENTS_ANIMATION,
+ CANCEL_RECENTS_ANIMATION, SET_ON_PAGE_TRANSITION_END_CALLBACK, CANCEL_CURRENT_ANIMATION,
+ CLEANUP_SCREENSHOT, SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED,
+ FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER,
/**
* These GestureEvents are specifically associated to state flags that get set in
@@ -91,35 +92,40 @@
case MOTION_UP:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.MOTION_DOWN),
- /* errorMessage= */ prefix + "\t\tMotion up detected before/without"
+ prefix,
+ /* errorMessage= */ "Motion up detected before/without"
+ " motion down.",
writer);
break;
case ON_SETTLED_ON_END_TARGET:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.SET_END_TARGET),
- /* errorMessage= */ prefix + "\t\tonSettledOnEndTarget called "
+ prefix,
+ /* errorMessage= */ "onSettledOnEndTarget called "
+ "before/without setEndTarget.",
writer);
break;
case FINISH_RECENTS_ANIMATION:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION),
- /* errorMessage= */ prefix + "\t\tfinishRecentsAnimation called "
+ prefix,
+ /* errorMessage= */ "finishRecentsAnimation called "
+ "before/without startRecentsAnimation.",
writer);
break;
case CANCEL_RECENTS_ANIMATION:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.START_RECENTS_ANIMATION),
- /* errorMessage= */ prefix + "\t\tcancelRecentsAnimation called "
+ prefix,
+ /* errorMessage= */ "cancelRecentsAnimation called "
+ "before/without startRecentsAnimation.",
writer);
break;
case CLEANUP_SCREENSHOT:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.STATE_SCREENSHOT_CAPTURED),
- /* errorMessage= */ prefix + "\t\trecents activity screenshot was "
+ prefix,
+ /* errorMessage= */ "recents activity screenshot was "
+ "cleaned up before/without STATE_SCREENSHOT_CAPTURED "
+ "being set.",
writer);
@@ -129,48 +135,66 @@
encounteredEvents.contains(GestureEvent.SET_END_TARGET_HOME)
&& !encounteredEvents.contains(
GestureEvent.ON_SETTLED_ON_END_TARGET),
- /* errorMessage= */ prefix + "\t\trecents view scroller animation "
+ prefix,
+ /* errorMessage= */ "recents view scroller animation "
+ "aborted after setting end target HOME, but before"
+ " settling on end target.",
writer);
break;
case TASK_APPEARED:
errorDetected |= printErrorIfTrue(
- !encounteredEvents.contains(GestureEvent.SET_END_TARGET_LAST_TASK)
- && !encounteredEvents.contains(
- GestureEvent.SET_END_TARGET_NEW_TASK),
- /* errorMessage= */ prefix + "\t\tonTasksAppeared called "
- + "before/without setting end target to last or new task",
+ !encounteredEvents.contains(GestureEvent.SET_END_TARGET_NEW_TASK),
+ prefix,
+ /* errorMessage= */ "onTasksAppeared called "
+ + "before/without setting end target to new task",
+ writer);
+ errorDetected |= printErrorIfTrue(
+ !encounteredEvents.contains(GestureEvent.EXPECTING_TASK_APPEARED),
+ prefix,
+ /* errorMessage= */ "onTasksAppeared was not expected to be called",
+ writer);
+ break;
+ case EXPECTING_TASK_APPEARED:
+ errorDetected |= printErrorIfTrue(
+ !encounteredEvents.contains(GestureEvent.SET_END_TARGET_NEW_TASK),
+ prefix,
+ /* errorMessage= */ "expecting onTasksAppeared to be called "
+ + "before/without setting end target to new task",
writer);
break;
case STATE_GESTURE_COMPLETED:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.MOTION_UP),
- /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_COMPLETED set "
+ prefix,
+ /* errorMessage= */ "STATE_GESTURE_COMPLETED set "
+ "before/without motion up.",
writer);
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED),
- /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_COMPLETED set "
+ prefix,
+ /* errorMessage= */ "STATE_GESTURE_COMPLETED set "
+ "before/without STATE_GESTURE_STARTED.",
writer);
break;
case STATE_GESTURE_CANCELLED:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.MOTION_UP),
- /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_CANCELLED set "
+ prefix,
+ /* errorMessage= */ "STATE_GESTURE_CANCELLED set "
+ "before/without motion up.",
writer);
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED),
- /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_CANCELLED set "
+ prefix,
+ /* errorMessage= */ "STATE_GESTURE_CANCELLED set "
+ "before/without STATE_GESTURE_STARTED.",
writer);
break;
case STATE_SCREENSHOT_CAPTURED:
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.STATE_CAPTURE_SCREENSHOT),
- /* errorMessage= */ prefix + "\t\tSTATE_SCREENSHOT_CAPTURED set "
+ prefix,
+ /* errorMessage= */ "STATE_SCREENSHOT_CAPTURED set "
+ "before/without STATE_CAPTURE_SCREENSHOT.",
writer);
break;
@@ -178,7 +202,8 @@
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(
GestureEvent.SET_ON_PAGE_TRANSITION_END_CALLBACK),
- /* errorMessage= */ prefix + "\t\tSTATE_RECENTS_SCROLLING_FINISHED "
+ prefix,
+ /* errorMessage= */ "STATE_RECENTS_SCROLLING_FINISHED "
+ "set before/without calling "
+ "setOnPageTransitionEndCallback.",
writer);
@@ -187,16 +212,19 @@
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(
GestureEvent.START_RECENTS_ANIMATION),
- /* errorMessage= */ prefix + "\t\tSTATE_RECENTS_ANIMATION_CANCELED "
+ prefix,
+ /* errorMessage= */ "STATE_RECENTS_ANIMATION_CANCELED "
+ "set before/without startRecentsAnimation.",
writer);
break;
case MOTION_DOWN:
case SET_END_TARGET:
case SET_END_TARGET_HOME:
+ case SET_END_TARGET_NEW_TASK:
case START_RECENTS_ANIMATION:
case SET_ON_PAGE_TRANSITION_END_CALLBACK:
case CANCEL_CURRENT_ANIMATION:
+ case FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER:
case STATE_GESTURE_STARTED:
case STATE_END_TARGET_ANIMATION_FINISHED:
case STATE_CAPTURE_SCREENSHOT:
@@ -210,31 +238,36 @@
// Check that all required events were found.
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.MOTION_DOWN),
- /* errorMessage= */ prefix + "\t\tMotion down never detected.",
+ prefix,
+ /* errorMessage= */ "Motion down never detected.",
writer);
errorDetected |= printErrorIfTrue(
!encounteredEvents.contains(GestureEvent.MOTION_UP),
- /* errorMessage= */ prefix + "\t\tMotion up never detected.",
+ prefix,
+ /* errorMessage= */ "Motion up never detected.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET)
&& !encounteredEvents.contains(GestureEvent.ON_SETTLED_ON_END_TARGET),
- /* errorMessage= */ prefix + "\t\tsetEndTarget was called, but "
+ prefix,
+ /* errorMessage= */ "setEndTarget was called, but "
+ "onSettledOnEndTarget wasn't.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET)
&& !encounteredEvents.contains(
GestureEvent.STATE_END_TARGET_ANIMATION_FINISHED),
- /* errorMessage= */ prefix + "\t\tsetEndTarget was called, but "
+ prefix,
+ /* errorMessage= */ "setEndTarget was called, but "
+ "STATE_END_TARGET_ANIMATION_FINISHED was never set.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(GestureEvent.SET_END_TARGET)
&& !encounteredEvents.contains(
GestureEvent.STATE_RECENTS_SCROLLING_FINISHED),
- /* errorMessage= */ prefix + "\t\tsetEndTarget was called, but "
+ prefix,
+ /* errorMessage= */ "setEndTarget was called, but "
+ "STATE_RECENTS_SCROLLING_FINISHED was never set.",
writer);
errorDetected |= printErrorIfTrue(
@@ -243,7 +276,8 @@
&& encounteredEvents.contains(
GestureEvent.STATE_RECENTS_SCROLLING_FINISHED)
&& !encounteredEvents.contains(GestureEvent.ON_SETTLED_ON_END_TARGET),
- /* errorMessage= */ prefix + "\t\tSTATE_END_TARGET_ANIMATION_FINISHED and "
+ prefix,
+ /* errorMessage= */ "STATE_END_TARGET_ANIMATION_FINISHED and "
+ "STATE_RECENTS_SCROLLING_FINISHED were set, but onSettledOnEndTarget "
+ "wasn't called.",
writer);
@@ -253,7 +287,8 @@
GestureEvent.START_RECENTS_ANIMATION)
&& !encounteredEvents.contains(GestureEvent.FINISH_RECENTS_ANIMATION)
&& !encounteredEvents.contains(GestureEvent.CANCEL_RECENTS_ANIMATION),
- /* errorMessage= */ prefix + "\t\tstartRecentsAnimation was called, but "
+ prefix,
+ /* errorMessage= */ "startRecentsAnimation was called, but "
+ "finishRecentsAnimation and cancelRecentsAnimation weren't.",
writer);
@@ -261,7 +296,8 @@
/* condition= */ encounteredEvents.contains(GestureEvent.STATE_GESTURE_STARTED)
&& !encounteredEvents.contains(GestureEvent.STATE_GESTURE_COMPLETED)
&& !encounteredEvents.contains(GestureEvent.STATE_GESTURE_CANCELLED),
- /* errorMessage= */ prefix + "\t\tSTATE_GESTURE_STARTED was set, but "
+ prefix,
+ /* errorMessage= */ "STATE_GESTURE_STARTED was set, but "
+ "STATE_GESTURE_COMPLETED and STATE_GESTURE_CANCELLED weren't.",
writer);
@@ -269,7 +305,8 @@
/* condition= */ encounteredEvents.contains(
GestureEvent.STATE_CAPTURE_SCREENSHOT)
&& !encounteredEvents.contains(GestureEvent.STATE_SCREENSHOT_CAPTURED),
- /* errorMessage= */ prefix + "\t\tSTATE_CAPTURE_SCREENSHOT was set, but "
+ prefix,
+ /* errorMessage= */ "STATE_CAPTURE_SCREENSHOT was set, but "
+ "STATE_SCREENSHOT_CAPTURED wasn't.",
writer);
@@ -278,15 +315,18 @@
GestureEvent.SET_ON_PAGE_TRANSITION_END_CALLBACK)
&& !encounteredEvents.contains(
GestureEvent.STATE_RECENTS_SCROLLING_FINISHED),
- /* errorMessage= */ prefix + "\t\tsetOnPageTransitionEndCallback called, but "
+ prefix,
+ /* errorMessage= */ "setOnPageTransitionEndCallback called, but "
+ "STATE_RECENTS_SCROLLING_FINISHED wasn't set.",
writer);
errorDetected |= printErrorIfTrue(
- /* condition= */ !encounteredEvents.contains(
- GestureEvent.CANCEL_CURRENT_ANIMATION)
+ /* condition= */ encounteredEvents.contains(
+ GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER)
+ && !encounteredEvents.contains(GestureEvent.CANCEL_CURRENT_ANIMATION)
&& !encounteredEvents.contains(GestureEvent.STATE_HANDLER_INVALIDATED),
- /* errorMessage= */ prefix + "\t\tAbsSwipeUpHandler.cancelCurrentAnimation "
+ prefix,
+ /* errorMessage= */ "AbsSwipeUpHandler.cancelCurrentAnimation "
+ "wasn't called and STATE_HANDLER_INVALIDATED wasn't set.",
writer);
@@ -294,23 +334,17 @@
/* condition= */ encounteredEvents.contains(
GestureEvent.STATE_RECENTS_ANIMATION_CANCELED)
&& !encounteredEvents.contains(GestureEvent.CLEANUP_SCREENSHOT),
- /* errorMessage= */ prefix + "\t\tSTATE_RECENTS_ANIMATION_CANCELED was set but "
+ prefix,
+ /* errorMessage= */ "STATE_RECENTS_ANIMATION_CANCELED was set but "
+ "the task screenshot wasn't cleaned up.",
writer);
errorDetected |= printErrorIfTrue(
/* condition= */ encounteredEvents.contains(
- GestureEvent.SET_END_TARGET_LAST_TASK)
+ GestureEvent.EXPECTING_TASK_APPEARED)
&& !encounteredEvents.contains(GestureEvent.TASK_APPEARED),
- /* errorMessage= */ prefix + "\t\tend target set to last task, but "
- + "onTaskAppeared wasn't called.",
- writer);
- errorDetected |= printErrorIfTrue(
- /* condition= */ encounteredEvents.contains(
- GestureEvent.SET_END_TARGET_NEW_TASK)
- && !encounteredEvents.contains(GestureEvent.TASK_APPEARED),
- /* errorMessage= */ prefix + "\t\tend target set to new task, but "
- + "onTaskAppeared wasn't called.",
+ prefix,
+ /* errorMessage= */ "onTaskAppeared was expected to be called but wasn't.",
writer);
if (!errorDetected) {
@@ -320,11 +354,11 @@
}
private static boolean printErrorIfTrue(
- boolean condition, String errorMessage, PrintWriter writer) {
+ boolean condition, String prefix, String errorMessage, PrintWriter writer) {
if (!condition) {
return false;
}
- writer.println(errorMessage);
+ writer.println(prefix + "\t\t- " + errorMessage);
return true;
}
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java b/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
deleted file mode 100644
index 483a1c6..0000000
--- a/quickstep/src/com/android/quickstep/util/SplitScreenBounds.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static android.view.Surface.ROTATION_0;
-import static android.view.Surface.ROTATION_180;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.WindowBounds;
-
-import java.util.ArrayList;
-
-/**
- * Utility class to hold the information abound a window bounds for split screen
- */
-@TargetApi(Build.VERSION_CODES.R)
-public class SplitScreenBounds {
-
- public static final SplitScreenBounds INSTANCE = new SplitScreenBounds();
- private final ArrayList<OnChangeListener> mListeners = new ArrayList<>();
-
- @Nullable
- private WindowBounds mBounds;
-
- private SplitScreenBounds() { }
-
- @UiThread
- public void setSecondaryWindowBounds(@NonNull WindowBounds bounds) {
- if (!bounds.equals(mBounds)) {
- mBounds = bounds;
- for (OnChangeListener listener : mListeners) {
- listener.onSecondaryWindowBoundsChanged();
- }
- }
- }
-
- public @NonNull WindowBounds getSecondaryWindowBounds(Context context) {
- if (mBounds == null) {
- mBounds = createDefaultWindowBounds(context);
- }
- return mBounds;
- }
-
- /**
- * Creates window bounds as 50% of device size
- */
- private static WindowBounds createDefaultWindowBounds(Context context) {
- WindowMetrics wm = context.getSystemService(WindowManager.class).getMaximumWindowMetrics();
- WindowBounds bounds = WindowBounds.fromWindowMetrics(wm);
-
- int rotation = DisplayController.INSTANCE.get(context).getInfo().rotation;
- int halfDividerSize = context.getResources()
- .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
-
- if (rotation == ROTATION_0 || rotation == ROTATION_180) {
- bounds.bounds.top = bounds.insets.top + bounds.availableSize.y / 2 + halfDividerSize;
- bounds.insets.top = 0;
- } else {
- bounds.bounds.left = bounds.insets.left + bounds.availableSize.x / 2 + halfDividerSize;
- bounds.insets.left = 0;
- }
- return new WindowBounds(bounds.bounds, bounds.insets);
- }
-
- public void addOnChangeListener(OnChangeListener listener) {
- mListeners.add(listener);
- }
-
- public void removeOnChangeListener(OnChangeListener listener) {
- mListeners.remove(listener);
- }
-
- /**
- * Interface to receive window bounds changes
- */
- public interface OnChangeListener {
-
- /**
- * Called when window bounds for secondary window changes
- */
- void onSecondaryWindowBoundsChanged();
- }
-}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 3119a77..08f9fa6 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -22,8 +22,10 @@
import static com.android.launcher3.Utilities.postAsyncCallback;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
+import static com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition;
import android.annotation.NonNull;
+import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.ActivityThread;
import android.app.PendingIntent;
@@ -33,6 +35,7 @@
import android.content.pm.ShortcutInfo;
import android.os.Handler;
import android.os.IBinder;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
@@ -40,6 +43,9 @@
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
import android.window.TransitionInfo;
import androidx.annotation.Nullable;
@@ -57,13 +63,11 @@
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskAnimationManager;
import com.android.quickstep.TaskViewUtils;
+import com.android.quickstep.views.FloatingTaskView;
import com.android.quickstep.views.GroupedTaskView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
-import com.android.systemui.shared.system.RemoteTransitionCompat;
-import com.android.systemui.shared.system.RemoteTransitionRunner;
import java.util.function.Consumer;
@@ -84,6 +88,8 @@
private ItemInfo mItemInfo;
private Intent mInitialTaskIntent;
private int mInitialTaskId = INVALID_TASK_ID;
+ private String mInitialTaskPackageName;
+ private Intent mSecondTaskIntent;
private int mSecondTaskId = INVALID_TASK_ID;
private String mSecondTaskPackageName;
private boolean mRecentsAnimationRunning;
@@ -95,6 +101,8 @@
/** Represents where split is intended to be invoked from. */
private StatsLogManager.EventEnum mSplitEvent;
+ private FloatingTaskView mFirstFloatingTaskView;
+
public SplitSelectStateController(Context context, Handler handler, StateManager stateManager,
DepthController depthController, StatsLogManager statsLogManager) {
mContext = context;
@@ -106,19 +114,36 @@
}
/**
- * To be called after first task selected
+ * To be called after first task selected in Overview.
*/
- public void setInitialTaskSelect(int taskId, @StagePosition int stagePosition,
+ public void setInitialTaskSelect(Task task, @StagePosition int stagePosition,
StatsLogManager.EventEnum splitEvent, ItemInfo itemInfo) {
- mInitialTaskId = taskId;
+ mInitialTaskId = task.key.id;
+ mInitialTaskPackageName = task.getTopComponent().getPackageName();
setInitialData(stagePosition, splitEvent, itemInfo);
}
+ /**
+ * To be called after first task selected from home or all apps.
+ */
public void setInitialTaskSelect(Intent intent, @StagePosition int stagePosition,
@NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent) {
mInitialTaskIntent = intent;
mUser = itemInfo.user;
mItemInfo = itemInfo;
+ mInitialTaskPackageName = intent.getComponent().getPackageName();
+ setInitialData(stagePosition, splitEvent, itemInfo);
+ }
+
+ /**
+ * To be called after first task selected from using a split shortcut from the fullscreen
+ * running app.
+ */
+ public void setInitialTaskSelect(ActivityManager.RunningTaskInfo info,
+ @StagePosition int stagePosition, @NonNull ItemInfo itemInfo,
+ StatsLogManager.EventEnum splitEvent) {
+ mInitialTaskId = info.taskId;
+ mInitialTaskPackageName = info.topActivity.getPackageName();
setInitialData(stagePosition, splitEvent, itemInfo);
}
@@ -134,27 +159,11 @@
* to be launched. Call after launcher side animations are complete.
*/
public void launchSplitTasks(Consumer<Boolean> callback) {
- final Intent fillInIntent;
- if (mInitialTaskIntent != null) {
- fillInIntent = new Intent();
- if (TextUtils.equals(mInitialTaskIntent.getComponent().getPackageName(),
- mSecondTaskPackageName)) {
- fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
- }
- } else {
- fillInIntent = null;
- }
-
- final PendingIntent pendingIntent = mInitialTaskIntent == null ? null : (mUser != null
- ? PendingIntent.getActivityAsUser(mContext, 0, mInitialTaskIntent,
- FLAG_MUTABLE, null /* options */, mUser)
- : PendingIntent.getActivity(mContext, 0, mInitialTaskIntent, FLAG_MUTABLE));
-
Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
LogUtils.getShellShareableInstanceId();
- launchTasks(mInitialTaskId, pendingIntent, fillInIntent, mSecondTaskId, mStagePosition,
- callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO,
- instanceIds.first);
+ launchTasks(mInitialTaskId, mInitialTaskIntent, mInitialTaskPackageName, mSecondTaskId,
+ mSecondTaskIntent, mSecondTaskPackageName, mStagePosition, callback,
+ false /* freezeTaskList */, DEFAULT_SPLIT_RATIO, instanceIds.first);
mStatsLogManager.logger()
.withItemInfo(mItemInfo)
@@ -162,23 +171,25 @@
.log(mSplitEvent);
}
-
/**
* To be called as soon as user selects the second task (even if animations aren't complete)
* @param task The second task that will be launched.
*/
public void setSecondTask(Task task) {
mSecondTaskId = task.key.id;
- if (mInitialTaskIntent != null) {
- mSecondTaskPackageName = task.getTopComponent().getPackageName();
- }
+ mSecondTaskPackageName = task.getTopComponent().getPackageName();
+ }
+
+ public void setSecondTask(Intent intent) {
+ mSecondTaskIntent = intent;
+ mSecondTaskPackageName = intent.getComponent().getPackageName();
}
/**
* To be called when we want to launch split pairs from an existing GroupedTaskView.
*/
- public void launchTasks(GroupedTaskView groupedTaskView,
- Consumer<Boolean> callback, boolean freezeTaskList) {
+ public void launchTasks(GroupedTaskView groupedTaskView, Consumer<Boolean> callback,
+ boolean freezeTaskList) {
mLaunchingTaskView = groupedTaskView;
TaskView.TaskIdAttributeContainer[] taskIdAttributeContainers =
groupedTaskView.getTaskIdAttributeContainers();
@@ -194,22 +205,23 @@
*/
public void launchTasks(int taskId1, int taskId2, @StagePosition int stagePosition,
Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio) {
- launchTasks(taskId1, null /* taskPendingIntent */, null /* fillInIntent */, taskId2,
- stagePosition, callback, freezeTaskList, splitRatio, null);
+ launchTasks(taskId1, null /* intent1 */, null /* packageName1 */, taskId2,
+ null /* intent2 */, null /* packageName2 */, stagePosition, callback,
+ freezeTaskList, splitRatio, null);
}
/**
* To be called when we want to launch split pairs from Overview. Split can be initiated from
* either Overview or home, or all apps. Either both taskIds are set, or a pending intent + a
* fill in intent with a taskId2 are set.
- * @param taskPendingIntent is null when split is initiated from Overview
+ * @param intent1 is null when split is initiated from Overview
* @param stagePosition representing location of task1
* @param shellInstanceId loggingId to be used by shell, will be non-null for actions that create
* a split instance, null for cases that bring existing instaces to the
* foreground (quickswitch, launching previous pairs from overview)
*/
- public void launchTasks(int taskId1, @Nullable PendingIntent taskPendingIntent,
- @Nullable Intent fillInIntent, int taskId2, @StagePosition int stagePosition,
+ public void launchTasks(int taskId1, @Nullable Intent intent1, String packageName1, int taskId2,
+ @Nullable Intent intent2, String packageName2, @StagePosition int stagePosition,
Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio,
@Nullable InstanceId shellInstanceId) {
TestLogging.recordEvent(
@@ -220,57 +232,105 @@
}
if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
final RemoteSplitLaunchTransitionRunner animationRunner =
- new RemoteSplitLaunchTransitionRunner(taskId1, taskPendingIntent, taskId2,
- callback);
- final RemoteTransitionCompat remoteTransition = new RemoteTransitionCompat(
- animationRunner, MAIN_EXECUTOR,
+ new RemoteSplitLaunchTransitionRunner(taskId1, taskId2, callback);
+ final RemoteTransition remoteTransition = new RemoteTransition(animationRunner,
ActivityThread.currentActivityThread().getApplicationThread());
- if (taskPendingIntent == null) {
+ if (intent1 == null && intent2 == null) {
mSystemUiProxy.startTasks(taskId1, options1.toBundle(), taskId2,
null /* options2 */, stagePosition, splitRatio, remoteTransition,
shellInstanceId);
+ } else if (intent2 == null) {
+ launchIntentOrShortcut(intent1, packageName2, options1, taskId2, stagePosition,
+ splitRatio, remoteTransition, shellInstanceId);
+ } else if (intent1 == null) {
+ launchIntentOrShortcut(intent2, packageName1, options1, taskId1,
+ getOppositeStagePosition(stagePosition), splitRatio, remoteTransition,
+ shellInstanceId);
} else {
- final ShortcutInfo shortcutInfo = getShortcutInfo(mInitialTaskIntent,
- taskPendingIntent.getCreatorUserHandle());
- if (shortcutInfo != null) {
- mSystemUiProxy.startShortcutAndTask(shortcutInfo,
- options1.toBundle(), taskId2, null /* options2 */, stagePosition,
- splitRatio, remoteTransition, shellInstanceId);
- } else {
- mSystemUiProxy.startIntentAndTask(taskPendingIntent,
- fillInIntent, options1.toBundle(), taskId2, null /* options2 */,
- stagePosition, splitRatio, remoteTransition, shellInstanceId);
- }
+ // TODO: the case when both split apps are started from an intent.
}
} else {
final RemoteSplitLaunchAnimationRunner animationRunner =
- new RemoteSplitLaunchAnimationRunner(taskId1, taskPendingIntent, taskId2,
- callback);
+ new RemoteSplitLaunchAnimationRunner(taskId1, taskId2, callback);
final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
- RemoteAnimationAdapterCompat.wrapRemoteAnimationRunner(animationRunner),
- 300, 150,
+ animationRunner, 300, 150,
ActivityThread.currentActivityThread().getApplicationThread());
- if (taskPendingIntent == null) {
+ if (intent1 == null && intent2 == null) {
mSystemUiProxy.startTasksWithLegacyTransition(taskId1, options1.toBundle(),
taskId2, null /* options2 */, stagePosition, splitRatio, adapter,
shellInstanceId);
+ } else if (intent2 == null) {
+ launchIntentOrShortcutLegacy(intent1, packageName2, options1, taskId2,
+ stagePosition, splitRatio, adapter, shellInstanceId);
+ } else if (intent1 == null) {
+ launchIntentOrShortcutLegacy(intent2, packageName1, options1, taskId1,
+ getOppositeStagePosition(stagePosition), splitRatio, adapter,
+ shellInstanceId);
} else {
- final ShortcutInfo shortcutInfo = getShortcutInfo(mInitialTaskIntent,
- taskPendingIntent.getCreatorUserHandle());
- if (shortcutInfo != null) {
- mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
- options1.toBundle(), taskId2, null /* options2 */, stagePosition,
- splitRatio, adapter, shellInstanceId);
- } else {
- mSystemUiProxy.startIntentAndTaskWithLegacyTransition(taskPendingIntent,
- fillInIntent, options1.toBundle(), taskId2, null /* options2 */,
- stagePosition, splitRatio, adapter, shellInstanceId);
- }
+ // TODO: the case when both split apps are started from an intent.
}
}
}
+ private void launchIntentOrShortcut(Intent intent, String otherTaskPackageName,
+ ActivityOptions options1, int taskId, @StagePosition int stagePosition,
+ float splitRatio, RemoteTransition remoteTransition,
+ @Nullable InstanceId shellInstanceId) {
+ PendingIntent pendingIntent = getPendingIntent(intent);
+ final ShortcutInfo shortcutInfo = getShortcutInfo(intent,
+ pendingIntent.getCreatorUserHandle());
+ if (shortcutInfo != null) {
+ mSystemUiProxy.startShortcutAndTask(shortcutInfo,
+ options1.toBundle(), taskId, null /* options2 */, stagePosition,
+ splitRatio, remoteTransition, shellInstanceId);
+ } else {
+ mSystemUiProxy.startIntentAndTask(pendingIntent,
+ getFillInIntent(intent, otherTaskPackageName), options1.toBundle(), taskId,
+ null /* options2 */, stagePosition, splitRatio, remoteTransition,
+ shellInstanceId);
+ }
+ }
+
+ private void launchIntentOrShortcutLegacy(Intent intent, String otherTaskPackageName,
+ ActivityOptions options1, int taskId, @StagePosition int stagePosition,
+ float splitRatio, RemoteAnimationAdapter adapter,
+ @Nullable InstanceId shellInstanceId) {
+ PendingIntent pendingIntent = getPendingIntent(intent);
+ final ShortcutInfo shortcutInfo = getShortcutInfo(intent,
+ pendingIntent.getCreatorUserHandle());
+ if (shortcutInfo != null) {
+ mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(shortcutInfo,
+ options1.toBundle(), taskId, null /* options2 */, stagePosition,
+ splitRatio, adapter, shellInstanceId);
+ } else {
+ mSystemUiProxy.startIntentAndTaskWithLegacyTransition(pendingIntent,
+ getFillInIntent(intent, otherTaskPackageName), options1.toBundle(), taskId,
+ null /* options2 */, stagePosition, splitRatio, adapter,
+ shellInstanceId);
+ }
+ }
+
+ private PendingIntent getPendingIntent(Intent intent) {
+ return intent == null ? null : (mUser != null
+ ? PendingIntent.getActivityAsUser(mContext, 0, intent,
+ FLAG_MUTABLE, null /* options */, mUser)
+ : PendingIntent.getActivity(mContext, 0, intent, FLAG_MUTABLE));
+ }
+
+ private Intent getFillInIntent(Intent intent, String otherTaskPackageName) {
+ if (intent == null) {
+ return null;
+ }
+
+ Intent fillInIntent = new Intent();
+ if (TextUtils.equals(intent.getComponent().getPackageName(), otherTaskPackageName)) {
+ fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ return fillInIntent;
+ }
+
+
public @StagePosition int getActiveSplitStagePosition() {
return mStagePosition;
}
@@ -280,7 +340,7 @@
}
public void setRecentsAnimationRunning(boolean running) {
- this.mRecentsAnimationRunning = running;
+ mRecentsAnimationRunning = running;
}
@Nullable
@@ -308,52 +368,63 @@
/**
* Requires Shell Transitions
*/
- private class RemoteSplitLaunchTransitionRunner implements RemoteTransitionRunner {
+ private class RemoteSplitLaunchTransitionRunner extends IRemoteTransition.Stub {
private final int mInitialTaskId;
- private final PendingIntent mInitialTaskPendingIntent;
private final int mSecondTaskId;
private final Consumer<Boolean> mSuccessCallback;
- RemoteSplitLaunchTransitionRunner(int initialTaskId, PendingIntent initialTaskPendingIntent,
- int secondTaskId, Consumer<Boolean> callback) {
+ RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId,
+ Consumer<Boolean> callback) {
mInitialTaskId = initialTaskId;
- mInitialTaskPendingIntent = initialTaskPendingIntent;
mSecondTaskId = secondTaskId;
mSuccessCallback = callback;
}
@Override
- public void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
- TaskViewUtils.composeRecentsSplitLaunchAnimator(mLaunchingTaskView, mStateManager,
- mDepthController, mInitialTaskId, mInitialTaskPendingIntent, mSecondTaskId,
- info, t, () -> {
- finishCallback.run();
- if (mSuccessCallback != null) {
- mSuccessCallback.accept(true);
- }
- });
- // After successful launch, call resetState
- resetState();
+ public void startAnimation(IBinder transition, TransitionInfo info,
+ SurfaceControl.Transaction t,
+ IRemoteTransitionFinishedCallback finishedCallback) {
+ final Runnable finishAdapter = () -> {
+ try {
+ finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to call transition finished callback", e);
+ }
+ };
+
+ MAIN_EXECUTOR.execute(() -> {
+ TaskViewUtils.composeRecentsSplitLaunchAnimator(mLaunchingTaskView, mStateManager,
+ mDepthController, mInitialTaskId, mSecondTaskId, info, t, () -> {
+ finishAdapter.run();
+ if (mSuccessCallback != null) {
+ mSuccessCallback.accept(true);
+ }
+ });
+ // After successful launch, call resetState
+ resetState();
+ });
}
+
+ @Override
+ public void mergeAnimation(IBinder transition, TransitionInfo info,
+ SurfaceControl.Transaction t, IBinder mergeTarget,
+ IRemoteTransitionFinishedCallback finishedCallback) { }
}
/**
* LEGACY
* Remote animation runner for animation to launch an app.
*/
- private class RemoteSplitLaunchAnimationRunner implements RemoteAnimationRunnerCompat {
+ private class RemoteSplitLaunchAnimationRunner extends RemoteAnimationRunnerCompat {
private final int mInitialTaskId;
- private final PendingIntent mInitialTaskPendingIntent;
private final int mSecondTaskId;
private final Consumer<Boolean> mSuccessCallback;
- RemoteSplitLaunchAnimationRunner(int initialTaskId, PendingIntent initialTaskPendingIntent,
- int secondTaskId, Consumer<Boolean> successCallback) {
+ RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId,
+ Consumer<Boolean> successCallback) {
mInitialTaskId = initialTaskId;
- mInitialTaskPendingIntent = initialTaskPendingIntent;
mSecondTaskId = secondTaskId;
mSuccessCallback = successCallback;
}
@@ -364,9 +435,8 @@
Runnable finishedCallback) {
postAsyncCallback(mHandler,
() -> TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
- mLaunchingTaskView, mInitialTaskId, mInitialTaskPendingIntent,
- mSecondTaskId, apps, wallpapers, nonApps, mStateManager,
- mDepthController, () -> {
+ mLaunchingTaskView, mInitialTaskId, mSecondTaskId, apps, wallpapers,
+ nonApps, mStateManager, mDepthController, () -> {
finishedCallback.run();
if (mSuccessCallback != null) {
mSuccessCallback.accept(true);
@@ -376,7 +446,7 @@
}
@Override
- public void onAnimationCancelled() {
+ public void onAnimationCancelled(boolean isKeyguardOccluded) {
postAsyncCallback(mHandler, () -> {
if (mSuccessCallback != null) {
// Launching legacy tasks while recents animation is running will always cause
@@ -394,7 +464,10 @@
public void resetState() {
mInitialTaskId = INVALID_TASK_ID;
mInitialTaskIntent = null;
+ mInitialTaskPackageName = null;
mSecondTaskId = INVALID_TASK_ID;
+ mSecondTaskIntent = null;
+ mSecondTaskPackageName = null;
mStagePosition = SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
mRecentsAnimationRunning = false;
mLaunchingTaskView = null;
@@ -407,7 +480,7 @@
* chosen
*/
public boolean isSplitSelectActive() {
- return isInitialTaskIntentSet() && mSecondTaskId == INVALID_TASK_ID;
+ return isInitialTaskIntentSet() && !isSecondTaskIntentSet();
}
/**
@@ -415,7 +488,7 @@
* be launched
*/
public boolean isBothSplitAppsConfirmed() {
- return isInitialTaskIntentSet() && mSecondTaskId != INVALID_TASK_ID;
+ return isInitialTaskIntentSet() && isSecondTaskIntentSet();
}
private boolean isInitialTaskIntentSet() {
@@ -425,4 +498,16 @@
public int getInitialTaskId() {
return mInitialTaskId;
}
+
+ private boolean isSecondTaskIntentSet() {
+ return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null);
+ }
+
+ public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) {
+ mFirstFloatingTaskView = floatingTaskView;
+ }
+
+ public FloatingTaskView getFirstFloatingTaskView() {
+ return mFirstFloatingTaskView;
+ }
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
new file mode 100644
index 0000000..3587bd1
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.ActivityManager;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.view.View;
+
+import androidx.annotation.BinderThread;
+
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.quickstep.OverviewCommandHelper;
+import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsAnimationCallbacks;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RecentsAnimationTargets;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.views.FloatingTaskView;
+import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+
+/** Transitions app from fullscreen to stage split when triggered from keyboard shortcuts. */
+public class SplitWithKeyboardShortcutController {
+
+ private final QuickstepLauncher mLauncher;
+ private final SplitSelectStateController mController;
+ private final OverviewComponentObserver mOverviewComponentObserver;
+
+ private final int mSplitPlaceholderSize;
+ private final int mSplitPlaceholderInset;
+
+ public SplitWithKeyboardShortcutController(QuickstepLauncher launcher,
+ SplitSelectStateController controller) {
+ mLauncher = launcher;
+ mController = controller;
+ RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(
+ launcher.getApplicationContext());
+ mOverviewComponentObserver = new OverviewComponentObserver(launcher.getApplicationContext(),
+ deviceState);
+
+ mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
+ R.dimen.split_placeholder_size);
+ mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize(
+ R.dimen.split_placeholder_inset);
+ }
+
+ @BinderThread
+ public void enterStageSplit(boolean leftOrTop) {
+ if (!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get()) {
+ return;
+ }
+ RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks(
+ SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()),
+ false /* allowMinimizeSplitScreen */);
+ SplitWithKeyboardShortcutRecentsAnimationListener listener =
+ new SplitWithKeyboardShortcutRecentsAnimationListener(leftOrTop);
+
+ MAIN_EXECUTOR.execute(() -> {
+ callbacks.addListener(listener);
+ UI_HELPER_EXECUTOR.execute(
+ // Transition from fullscreen app to enter stage split in launcher with
+ // recents animation.
+ () -> ActivityManagerWrapper.getInstance().startRecentsActivity(
+ mOverviewComponentObserver.getOverviewIntent(),
+ SystemClock.uptimeMillis(), callbacks, null, null));
+ });
+ }
+
+ /**
+ * Handles second app selection from stage split. If the item can't be opened in split or
+ * it's not in stage split state, we pass it onto Launcher's default item click handler.
+ */
+ public boolean handleSecondAppSelectionForSplit(View view) {
+ if (!ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS.get()
+ || !mController.isSplitSelectActive()) {
+ return false;
+ }
+ Object tag = view.getTag();
+ Intent intent;
+ if (tag instanceof WorkspaceItemInfo) {
+ final WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) tag;
+ intent = workspaceItemInfo.intent;
+ } else if (tag instanceof com.android.launcher3.model.data.AppInfo) {
+ final com.android.launcher3.model.data.AppInfo appInfo =
+ (com.android.launcher3.model.data.AppInfo) tag;
+ intent = appInfo.intent;
+ } else {
+ return false;
+ }
+ mController.setSecondTask(intent);
+ mController.launchSplitTasks(aBoolean -> mLauncher.getDragLayer().removeView(
+ mController.getFirstFloatingTaskView()));
+ return true;
+ }
+
+ public void onDestroy() {
+ mOverviewComponentObserver.onDestroy();
+ }
+
+ private class SplitWithKeyboardShortcutRecentsAnimationListener implements
+ RecentsAnimationCallbacks.RecentsAnimationListener {
+
+ private final boolean mLeftOrTop;
+ private final Rect mTempRect = new Rect();
+
+ private SplitWithKeyboardShortcutRecentsAnimationListener(boolean leftOrTop) {
+ mLeftOrTop = leftOrTop;
+ }
+
+ @Override
+ public void onRecentsAnimationStart(RecentsAnimationController controller,
+ RecentsAnimationTargets targets) {
+ ActivityManager.RunningTaskInfo runningTaskInfo =
+ ActivityManagerWrapper.getInstance().getRunningTask();
+ mController.setInitialTaskSelect(runningTaskInfo,
+ mLeftOrTop ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT,
+ null /* itemInfo */,
+ mLeftOrTop ? LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP
+ : LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM);
+
+ RecentsView recentsView = mLauncher.getOverviewPanel();
+ recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds(
+ mSplitPlaceholderSize, mSplitPlaceholderInset, mLauncher.getDeviceProfile(),
+ mController.getActiveSplitStagePosition(), mTempRect);
+
+ PendingAnimation anim = new PendingAnimation(
+ SplitAnimationTimings.TABLET_HOME_TO_SPLIT.getDuration());
+ RectF startingTaskRect = new RectF();
+ final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(
+ mLauncher, mLauncher.getDragLayer(),
+ controller.screenshotTask(runningTaskInfo.taskId).thumbnail,
+ null /* icon */, startingTaskRect);
+ RecentsModel.INSTANCE.get(mLauncher.getApplicationContext())
+ .getIconCache()
+ .updateIconInBackground(
+ Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
+ false /* isLocked */),
+ (task) -> {
+ if (task.thumbnail != null) {
+ floatingTaskView.setIcon(task.thumbnail.thumbnail);
+ }
+ });
+ floatingTaskView.setAlpha(1);
+ floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
+ false /* fadeWithThumbnail */, true /* isStagedTask */);
+ mController.setFirstFloatingTaskView(floatingTaskView);
+
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ controller.finish(true /* toRecents */, null /* onFinishComplete */,
+ false /* sendUserLeaveHint */);
+ }
+ });
+ anim.buildAnim().start();
+ }
+ };
+}
diff --git a/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
index 580cc99..3756b4a 100644
--- a/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
+++ b/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
@@ -26,7 +26,7 @@
public int getPlaceholderIconFadeInStart() { return 167; }
public int getPlaceholderIconFadeInEnd() { return 250; }
public int getStagedRectSlideStart() { return 0; }
- public int getStagedRectSlideEnd() { return 383; }
+ public int getStagedRectSlideEnd() { return 500; }
public int getDuration() { return TABLET_CONFIRM_DURATION; }
}
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
index 50be5ea..d098ffc 100644
--- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
@@ -99,6 +99,10 @@
return false;
}
+ public float getScrollAlpha() {
+ return mScrollAlpha;
+ }
+
public void setContentAlpha(float alpha) {
if (mContentAlpha != alpha) {
mContentAlpha = alpha;
diff --git a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
index dc1ae52..1d421b2 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/FloatingTaskView.java
@@ -11,6 +11,7 @@
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.FloatProperty;
@@ -74,6 +75,7 @@
}
};
+ private int mSplitHolderSize;
private FloatingTaskThumbnailView mThumbnailView;
private SplitPlaceholderView mSplitPlaceholderView;
private RectF mStartingPosition;
@@ -97,6 +99,9 @@
mActivity = BaseActivity.fromContext(context);
mIsRtl = Utilities.isRtl(getResources());
mFullscreenParams = new FullscreenDrawParams(context);
+
+ mSplitHolderSize = context.getResources().getDimensionPixelSize(
+ R.dimen.split_placeholder_icon_size);
}
@Override
@@ -126,8 +131,7 @@
RecentsView recentsView = launcher.getOverviewPanel();
mOrientationHandler = recentsView.getPagedOrientationHandler();
mStagePosition = recentsView.getSplitSelectController().getActiveSplitStagePosition();
- mSplitPlaceholderView.setIcon(icon,
- mContext.getResources().getDimensionPixelSize(R.dimen.split_placeholder_icon_size));
+ mSplitPlaceholderView.setIcon(icon, mSplitHolderSize);
mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
}
@@ -193,6 +197,10 @@
mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
}
+ public void setIcon(Bitmap icon) {
+ mSplitPlaceholderView.setIcon(new BitmapDrawable(icon), mSplitHolderSize);
+ }
+
protected void initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
mStartingPosition.set(pos);
lp.ignoreInsets = true;
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index a16ff8f..eeabdc8 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -82,6 +82,8 @@
private static final int INDEX_FULLSCREEN_ALPHA = 2;
private static final int INDEX_HIDDEN_FLAGS_ALPHA = 3;
private static final int INDEX_SHARE_TARGET_ALPHA = 4;
+ private static final int INDEX_SCROLL_ALPHA = 5;
+ private static final int NUM_ALPHAS = 6;
public @interface SplitButtonHiddenFlags { }
public static final int FLAG_IS_NOT_TABLET = 1 << 0;
@@ -126,7 +128,7 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
- mMultiValueAlpha = new MultiValueAlpha(findViewById(R.id.action_buttons), 5);
+ mMultiValueAlpha = new MultiValueAlpha(findViewById(R.id.action_buttons), NUM_ALPHAS);
mMultiValueAlpha.setUpdateVisibility(true);
findViewById(R.id.action_screenshot).setOnClickListener(this);
@@ -247,6 +249,10 @@
return mMultiValueAlpha.get(INDEX_SHARE_TARGET_ALPHA);
}
+ public MultiProperty getIndexScrollAlpha() {
+ return mMultiValueAlpha.get(INDEX_SCROLL_ALPHA);
+ }
+
/**
* Offsets OverviewActionsView horizontal position based on 3 button nav container in taskbar.
*/
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index ce96b71..47374e0 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -122,6 +122,7 @@
import androidx.annotation.UiThread;
import androidx.core.graphics.ColorUtils;
+import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
import com.android.launcher3.DeviceProfile;
@@ -178,7 +179,6 @@
import com.android.quickstep.util.LayoutUtils;
import com.android.quickstep.util.RecentsOrientedState;
import com.android.quickstep.util.SplitAnimationTimings;
-import com.android.quickstep.util.SplitScreenBounds;
import com.android.quickstep.util.SplitSelectStateController;
import com.android.quickstep.util.SurfaceTransaction;
import com.android.quickstep.util.SurfaceTransactionApplier;
@@ -210,7 +210,7 @@
public abstract class RecentsView<ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>,
STATE_TYPE extends BaseState<STATE_TYPE>> extends PagedView implements Insettable,
TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
- TaskVisualsChangeListener, SplitScreenBounds.OnChangeListener {
+ TaskVisualsChangeListener {
private static final String TAG = "RecentsView";
private static final boolean DEBUG = false;
@@ -948,7 +948,6 @@
SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(
mIPipAnimationListener);
mOrientationState.initListeners();
- SplitScreenBounds.INSTANCE.addOnChangeListener(this);
mTaskOverlayFactory.initListeners();
}
@@ -965,7 +964,6 @@
executeSideTaskLaunchCallback();
RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
SystemUiProxy.INSTANCE.get(getContext()).setPipAnimationListener(null);
- SplitScreenBounds.INSTANCE.removeOnChangeListener(this);
mIPipAnimationListener.setActivityAndRecentsView(null, null);
mOrientationState.destroyListeners();
mTaskOverlayFactory.removeListeners();
@@ -1913,6 +1911,9 @@
}
int scroll = mOrientationHandler.getPrimaryScroll(this);
mClearAllButton.onRecentsViewScroll(scroll, mOverviewGridEnabled);
+
+ // Clear all button alpha was set by the previous line.
+ mActionsView.getIndexScrollAlpha().setValue(1 - mClearAllButton.getScrollAlpha());
}
@Override
@@ -2241,8 +2242,14 @@
for (int i = 0; i < getTaskViewCount(); i++) {
requireTaskViewAt(i).setOrientationState(mOrientationState);
}
+ boolean shouldRotateMenuForFakeRotation =
+ !mOrientationState.isRecentsActivityRotationAllowed();
+ if (!shouldRotateMenuForFakeRotation) {
+ return;
+ }
TaskMenuView tv = (TaskMenuView) getTopOpenViewWithType(mActivity, TYPE_TASK_MENU);
if (tv != null) {
+ // Rotation is supported on phone (details at b/254198019#comment4)
tv.onRotationChanged();
}
}
@@ -2897,6 +2904,19 @@
InteractionJankMonitorWrapper.begin(this,
InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "First tile selected");
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (mSplitHiddenTaskView == getRunningTaskView()) {
+ finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
+ null /* onFinishComplete */);
+ } else {
+ switchToScreenshot(
+ () -> finishRecentsAnimation(true /* toRecents */,
+ false /* shouldPip */, null /* onFinishComplete */));
+ }
+ }
+ });
anim.addEndListener(success -> {
if (success) {
InteractionJankMonitorWrapper.end(
@@ -4184,11 +4204,9 @@
public void initiateSplitSelect(TaskView taskView, @StagePosition int stagePosition,
StatsLogManager.EventEnum splitEvent) {
mSplitHiddenTaskView = taskView;
- mSplitSelectStateController.setInitialTaskSelect(taskView.getTask().key.id,
+ mSplitSelectStateController.setInitialTaskSelect(taskView.getTask(),
stagePosition, splitEvent, taskView.getItemInfo());
mSplitHiddenTaskViewIndex = indexOfChild(taskView);
- finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
- null /* onFinishComplete */);
}
/**
@@ -5195,12 +5213,6 @@
return null;
}
- @Override
- public void onSecondaryWindowBoundsChanged() {
- // Invalidate the task view size
- setInsets(mInsets);
- }
-
/** Enables or disables modal state for RecentsView */
public abstract void setModalStateEnabled(boolean isModalState, boolean animate);
diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
index b903691..83341cb 100644
--- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
+++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -41,7 +41,6 @@
import androidx.test.filters.SmallTest;
import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
@@ -136,21 +135,21 @@
public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder()
throws Exception {
// WHEN newPredicationTask is executed with app predication of 5 apps.
- AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "className",
+ AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1",
mUserHandle);
- AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "className",
+ AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "provider1",
mUserHandle);
AppTarget app3 = new AppTarget(new AppTargetId("app3"), "app3", "className",
mUserHandle);
- AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "className",
+ AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1",
mUserHandle);
- AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "className",
+ AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1",
mUserHandle);
mModelHelper.executeTaskForTest(
newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1)))
.forEach(Runnable::run);
- // THEN only 3 widgets are returned because
+ // THEN only 2 widgets are returned because
// 1. app5/provider1 & app4/provider1 have already been added to workspace. They are
// excluded from the result.
// 2. app3 doesn't have a widget.
@@ -159,45 +158,39 @@
.stream()
.map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
.collect(Collectors.toList());
- assertThat(recommendedWidgets).hasSize(3);
+ assertThat(recommendedWidgets).hasSize(2);
assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1);
- assertWidgetInfo(recommendedWidgets.get(1).info, mApp4Provider2);
- assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1);
+ assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1);
}
@Test
- public void widgetsRecommendationRan_localFilterDisabled_shouldReturnWidgetsInPredicationOrder()
+ public void widgetsRecommendationRan_shouldReturnPackageWidgetsWhenEmpty()
throws Exception {
- if (FeatureFlags.ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER.get()) {
- return;
- }
- // WHEN newPredicationTask is executed with 5 predicated widgets.
- AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1",
- mUserHandle);
- AppTarget widget2 = new AppTarget(new AppTargetId("app1"), "app1", "provider2",
+ // Not installed widget
+ AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider3",
mUserHandle);
// Not installed app
AppTarget widget3 = new AppTarget(new AppTargetId("app2"), "app3", "provider1",
mUserHandle);
- // Not installed widget
- AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider3",
+ // Workspace added widgets
+ AppTarget widget4 = new AppTarget(new AppTargetId("app4"), "app4", "provider1",
mUserHandle);
AppTarget widget5 = new AppTarget(new AppTargetId("app5"), "app5", "provider1",
mUserHandle);
mModelHelper.executeTaskForTest(
- newWidgetsPredicationTask(List.of(widget5, widget3, widget2, widget4, widget1)))
+ newWidgetsPredicationTask(List.of(widget5, widget3, widget4, widget1)))
.forEach(Runnable::run);
- // THEN only 3 widgets are returned because the launcher only filters out non-exist widgets.
+ // THEN only 2 widgets are returned because the launcher only filters out non-exist widgets.
List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
.stream()
.map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
.collect(Collectors.toList());
- assertThat(recommendedWidgets).hasSize(3);
- assertWidgetInfo(recommendedWidgets.get(0).info, mApp5Provider1);
- assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider2);
- assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1);
+ assertThat(recommendedWidgets).hasSize(2);
+ // Another widget from the same package
+ assertWidgetInfo(recommendedWidgets.get(0).info, mApp4Provider2);
+ assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1);
}
private void assertWidgetInfo(
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index 39f93d9..5518dc8 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -30,11 +30,11 @@
<LinearLayout
android:id="@+id/folder_footer"
android:layout_width="match_parent"
- android:layout_height="@dimen/folder_label_height"
+ android:layout_height="@dimen/folder_footer_height_default"
android:clipChildren="false"
android:orientation="horizontal"
- android:paddingLeft="12dp"
- android:paddingRight="12dp">
+ android:paddingLeft="@dimen/folder_footer_horiz_padding"
+ android:paddingRight="@dimen/folder_footer_horiz_padding">
<com.android.launcher3.folder.FolderNameEditText
android:id="@+id/folder_name"
diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml
index 9da3e87..b2a3a0d 100644
--- a/res/layout/widgets_full_sheet_recyclerview.xml
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -30,6 +30,7 @@
android:layout_height="wrap_content"
android:layout_below="@id/collapse_handle"
android:paddingBottom="16dp"
+ android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
android:orientation="vertical">
<TextView
@@ -40,7 +41,6 @@
android:textSize="24sp"
android:layout_marginTop="24dp"
android:textColor="?android:attr/textColorSecondary"
- android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
android:text="@string/widget_button_text"/>
<FrameLayout
@@ -49,7 +49,6 @@
android:layout_height="wrap_content"
android:elevation="0.1dp"
android:background="?android:attr/colorBackground"
- android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
android:paddingBottom="8dp"
android:clipToPadding="false"
launcher:layout_sticky="true" >
@@ -63,7 +62,6 @@
android:layout_marginTop="8dp"
android:background="@drawable/widgets_recommendation_background"
android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
- android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
android:visibility="gone" />
</com.android.launcher3.views.StickyHeaderLayout>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 9e2d9a4..8b944fe 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -124,7 +124,7 @@
<string name="app_installing_title" msgid="5864044122733792085">"<xliff:g id="NAME">%1$s</xliff:g> instalatzen, <xliff:g id="PROGRESS">%2$s</xliff:g> osatuta"</string>
<string name="app_downloading_title" msgid="8336702962104482644">"<xliff:g id="NAME">%1$s</xliff:g> deskargatzen, <xliff:g id="PROGRESS">%2$s</xliff:g> osatuta"</string>
<string name="app_waiting_download_title" msgid="7053938513995617849">"<xliff:g id="NAME">%1$s</xliff:g> instalatzeko zain"</string>
- <string name="dialog_update_title" msgid="114234265740994042">"Aplikazioa eguneratu behar da"</string>
+ <string name="dialog_update_title" msgid="114234265740994042">"Aplikazioa eguneratu egin behar da"</string>
<string name="dialog_update_message" msgid="4176784553982226114">"Ikonoaren aplikazioa ez dago eguneratuta. Lasterbidea berriro gaitzeko, eskuz egunera dezakezu aplikazioa. Bestela, kendu ikonoa."</string>
<string name="dialog_update" msgid="2178028071796141234">"Eguneratu"</string>
<string name="dialog_remove" msgid="6510806469849709407">"Kendu"</string>
diff --git a/res/values-sw720dp/dimens.xml b/res/values-sw720dp/dimens.xml
index 7b2ed8b..09b2d6f 100644
--- a/res/values-sw720dp/dimens.xml
+++ b/res/values-sw720dp/dimens.xml
@@ -43,4 +43,7 @@
<!-- Bottom sheet-->
<dimen name="bottom_sheet_extra_top_padding">300dp</dimen>
+
+ <!-- Folder spaces -->
+ <dimen name="folder_footer_horiz_padding">24dp</dimen>
</resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 283c793..f270b10 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -141,9 +141,12 @@
<attr name="numColumns" format="integer" />
<!-- numSearchContainerColumns defaults to numColumns, if not specified -->
<attr name="numSearchContainerColumns" format="integer" />
+
<!-- numFolderRows & numFolderColumns defaults to numRows & numColumns, if not specified -->
<attr name="numFolderRows" format="integer" />
<attr name="numFolderColumns" format="integer" />
+ <attr name="folderStyle" format="reference" />
+
<!-- numAllAppsColumns defaults to numColumns, if not specified -->
<attr name="numAllAppsColumns" format="integer" />
<!-- Number of columns to use when extending the all-apps size,
@@ -333,19 +336,6 @@
if not specified -->
<attr name="allAppsBorderSpaceTwoPanelLandscapeVertical" format="float" />
- <!-- defaults to minCellHeight if not specified
- when GridDisplayOption#isScalable is true. -->
- <attr name="folderCellHeight" format="float" />
- <!-- defaults to minCellWidth, if not specified -->
- <attr name="folderCellWidth" format="float" />
-
- <!-- defaults to borderSpace, if not specified -->
- <!-- space to be used horizontally and vertically -->
- <attr name="folderBorderSpace" format="float" />
-
- <!-- defaults to folderBorderSpace vertical, if not specified -->
- <attr name="folderTopPadding" format="float" />
-
<!-- defaults to res.hotseat_bar_bottom_space_default, if not specified -->
<attr name="hotseatBarBottomSpace" format="float" />
<!-- defaults to hotseatBarBottomSpace, if not specified -->
@@ -394,6 +384,22 @@
</declare-styleable>
+ <declare-styleable name="FolderDisplayStyle">
+ <!-- defaults to minCellHeight if not specified
+ when GridDisplayOption#isScalable is true. -->
+ <attr name="folderCellHeight" format="dimension" />
+ <!-- defaults to minCellWidth, if not specified -->
+ <attr name="folderCellWidth" format="dimension" />
+ <!-- space to be used horizontally and vertically between icons,
+ and to the left and right of folder -->
+ <attr name="folderBorderSpace" format="dimension" />
+ <!-- height of the footer of the folder -->
+ <attr name="folderFooterHeight" format="dimension" />
+ <!-- padding on top of the folder -->
+ <attr name="folderTopPadding" format="dimension" />
+ </declare-styleable>
+
+
<declare-styleable name="CellLayout">
<attr name="containerType" format="integer">
<enum name="workspace" value="0" />
diff --git a/res/values/config.xml b/res/values/config.xml
index 11b6e8c..d9b3da5 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -205,6 +205,4 @@
<!-- The max scale for the wallpaper when it's zoomed in -->
<item name="config_wallpaperMaxScale" format="float" type="dimen">0</item>
- <string name="floating_task_package" translatable="false"></string>
- <string name="floating_task_action" translatable="false"></string>
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 0a28b6c..033346a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -255,7 +255,7 @@
<dimen name="folder_cell_y_padding">6dp</dimen>
<!-- label text size = workspace text size multiplied by this scale -->
<dimen name="folder_label_text_scale">1.14</dimen>
- <dimen name="folder_label_height">56dp</dimen>
+ <dimen name="folder_footer_height_default">56dp</dimen>
<dimen name="folder_content_padding_left_right">8dp</dimen>
<dimen name="folder_content_padding_top">16dp</dimen>
@@ -367,7 +367,9 @@
<dimen name="transient_taskbar_margin">0dp</dimen>
<dimen name="transient_taskbar_shadow_blur">0dp</dimen>
<dimen name="transient_taskbar_key_shadow_distance">0dp</dimen>
- <dimen name="transient_taskbar_icon_spacing">10dp</dimen>
+ <dimen name="transient_taskbar_stashed_size">0dp</dimen>
+ <!-- Note that this applies to both sides of all icons, so visible space is double this. -->
+ <dimen name="transient_taskbar_icon_spacing">0dp</dimen>
<!-- Note that this applies to both sides of all icons, so visible space is double this. -->
<dimen name="taskbar_icon_spacing">8dp</dimen>
<dimen name="taskbar_nav_buttons_size">0dp</dimen>
@@ -380,6 +382,11 @@
<dimen name="taskbar_button_margin_6_5">0dp</dimen>
<dimen name="taskbar_button_margin_4_5">0dp</dimen>
<dimen name="taskbar_button_margin_4_4">0dp</dimen>
+ <!-- Taskbar swipe up thresholds threshold -->
+ <dimen name="taskbar_nav_threshold">0dp</dimen>
+ <dimen name="taskbar_app_window_threshold">0dp</dimen>
+ <dimen name="taskbar_home_overview_threshold">0dp</dimen>
+ <dimen name="taskbar_catch_up_threshold">0dp</dimen>
<!-- Size of the maximum radius for the enforced rounded rectangles. -->
<dimen name="enforced_rounded_corner_max_radius">16dp</dimen>
@@ -434,4 +441,8 @@
<!-- State transition -->
<item name="workspace_content_scale" format="float" type="dimen">0.97</item>
+
+ <!-- Folder spaces -->
+ <dimen name="folder_top_padding_default">24dp</dimen>
+ <dimen name="folder_footer_horiz_padding">20dp</dimen>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index d0be420..9e75a31 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -309,4 +309,13 @@
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowTranslucentStatus">true</item>
</style>
+
+ <style name="FolderDefaultStyle">
+ <item name="folderTopPadding">24dp</item>
+ <item name="folderCellHeight">94dp</item>
+ <item name="folderCellWidth">80dp</item>
+ <item name="folderBorderSpace">16dp</item>
+ <item name="folderFooterHeight">56dp</item>
+ </style>
+
</resources>
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 75d7b6b..2103593 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -192,7 +192,7 @@
private final Rect mOccupiedRect = new Rect();
private final int[] mDirectionVector = new int[2];
- final int[] mPreviousReorderDirection = new int[2];
+ ItemConfiguration mPreviousSolution = null;
private static final int INVALID_DIRECTION = -100;
private final Rect mTempRect = new Rect();
@@ -246,9 +246,6 @@
mOccupied = new GridOccupancy(mCountX, mCountY);
mTmpOccupied = new GridOccupancy(mCountX, mCountY);
- mPreviousReorderDirection[0] = INVALID_DIRECTION;
- mPreviousReorderDirection[1] = INVALID_DIRECTION;
-
mFolderLeaveBehind.mDelegateCellX = -1;
mFolderLeaveBehind.mDelegateCellY = -1;
@@ -376,7 +373,8 @@
private void resetCellSizeInternal(DeviceProfile deviceProfile) {
switch (mContainerType) {
case FOLDER:
- mBorderSpace = new Point(deviceProfile.folderCellLayoutBorderSpacePx);
+ mBorderSpace = new Point(deviceProfile.folderCellLayoutBorderSpacePx,
+ deviceProfile.folderCellLayoutBorderSpacePx);
break;
case HOTSEAT:
mBorderSpace = new Point(deviceProfile.hotseatBorderSpace,
@@ -405,7 +403,6 @@
mCountY = y;
mOccupied = new GridOccupancy(mCountX, mCountY);
mTmpOccupied = new GridOccupancy(mCountX, mCountY);
- mTempRectStack.clear();
mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
mBorderSpace);
requestLayout();
@@ -941,7 +938,7 @@
cellToRect(targetCell[0], targetCell[1], spanX, spanY, cellBoundsWithSpacing);
cellBoundsWithSpacing.inset(-mBorderSpace.x / 2, -mBorderSpace.y / 2);
- if (canCreateFolder(getChildAt(targetCell[0], targetCell[1]))) {
+ if (canCreateFolder(getChildAt(targetCell[0], targetCell[1])) && spanX == 1 && spanY == 1) {
// Take only the circle in the smaller dimension, to ensure we don't start reordering
// too soon before accepting a folder drop.
int minRadius = centerPoint[0] - cellBoundsWithSpacing.left;
@@ -951,8 +948,9 @@
return minRadius;
}
// Take up the entire cell, including space between this cell and the adjacent ones.
- return (float) Math.hypot(cellBoundsWithSpacing.width() / 2f,
- cellBoundsWithSpacing.height() / 2f);
+ // Multiply by span to scale radius
+ return (float) Math.hypot(spanX * cellBoundsWithSpacing.width() / 2f,
+ spanY * cellBoundsWithSpacing.height() / 2f);
}
public int getCellWidth() {
@@ -1249,21 +1247,6 @@
result, resultSpan);
}
- private final Stack<Rect> mTempRectStack = new Stack<>();
- private void lazyInitTempRectStack() {
- if (mTempRectStack.isEmpty()) {
- for (int i = 0; i < mCountX * mCountY; i++) {
- mTempRectStack.push(new Rect());
- }
- }
- }
-
- private void recycleTempRects(Stack<Rect> used) {
- while (!used.isEmpty()) {
- mTempRectStack.push(used.pop());
- }
- }
-
/**
* Find a vacant area that will fit the given bounds nearest the requested
* cell location. Uses Euclidean distance to score multiple vacant areas.
@@ -1283,8 +1266,6 @@
*/
private int[] findNearestArea(int relativeXPos, int relativeYPos, int minSpanX, int minSpanY,
int spanX, int spanY, boolean ignoreOccupied, int[] result, int[] resultSpan) {
- lazyInitTempRectStack();
-
// For items with a spanX / spanY > 1, the passed in point (relativeXPos, relativeYPos)
// corresponds to the center of the item, but we are searching based on the top-left cell,
// so we translate the point over to correspond to the top-left.
@@ -1354,9 +1335,6 @@
hitMaxY |= ySize >= spanY;
incX = !incX;
}
- incX = true;
- hitMaxX = xSize >= spanX;
- hitMaxY = ySize >= spanY;
}
final int[] cellXY = mTmpPoint;
cellToCenterPoint(x, y, cellXY);
@@ -1364,8 +1342,7 @@
// We verify that the current rect is not a sub-rect of any of our previous
// candidates. In this case, the current rect is disqualified in favour of the
// containing rect.
- Rect currentRect = mTempRectStack.pop();
- currentRect.set(x, y, x + xSize, y + ySize);
+ Rect currentRect = new Rect(x, y, x + xSize, y + ySize);
boolean contained = false;
for (Rect r : validRegions) {
if (r.contains(currentRect)) {
@@ -1395,653 +1372,9 @@
bestXY[0] = -1;
bestXY[1] = -1;
}
- recycleTempRects(validRegions);
return bestXY;
}
- /**
- * Find a vacant area that will fit the given bounds nearest the requested
- * cell location, and will also weigh in a suggested direction vector of the
- * desired location. This method computers distance based on unit grid distances,
- * not pixel distances.
- *
- * @param cellX The X cell nearest to which you want to search for a vacant area.
- * @param cellY The Y cell nearest which you want to search for a vacant area.
- * @param spanX Horizontal span of the object.
- * @param spanY Vertical span of the object.
- * @param direction The favored direction in which the views should move from x, y
- * @param occupied The array which represents which cells in the CellLayout are occupied
- * @param blockOccupied The array which represents which cells in the specified block (cellX,
- * cellY, spanX, spanY) are occupied. This is used when try to move a group of views.
- * @param result Array in which to place the result, or null (in which case a new array will
- * be allocated)
- * @return The X, Y cell of a vacant area that can contain this object,
- * nearest the requested location.
- */
- private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction,
- boolean[][] occupied, boolean blockOccupied[][], int[] result) {
- // Keep track of best-scoring drop area
- final int[] bestXY = result != null ? result : new int[2];
- float bestDistance = Float.MAX_VALUE;
- int bestDirectionScore = Integer.MIN_VALUE;
-
- final int countX = mCountX;
- final int countY = mCountY;
-
- for (int y = 0; y < countY - (spanY - 1); y++) {
- inner:
- for (int x = 0; x < countX - (spanX - 1); x++) {
- // First, let's see if this thing fits anywhere
- for (int i = 0; i < spanX; i++) {
- for (int j = 0; j < spanY; j++) {
- if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) {
- continue inner;
- }
- }
- }
-
- float distance = (float) Math.hypot(x - cellX, y - cellY);
- int[] curDirection = mTmpPoint;
- computeDirectionVector(x - cellX, y - cellY, curDirection);
- // The direction score is just the dot product of the two candidate direction
- // and that passed in.
- int curDirectionScore = direction[0] * curDirection[0] +
- direction[1] * curDirection[1];
- if (Float.compare(distance, bestDistance) < 0 ||
- (Float.compare(distance, bestDistance) == 0
- && curDirectionScore > bestDirectionScore)) {
- bestDistance = distance;
- bestDirectionScore = curDirectionScore;
- bestXY[0] = x;
- bestXY[1] = y;
- }
- }
- }
-
- // Return -1, -1 if no suitable location found
- if (bestDistance == Float.MAX_VALUE) {
- bestXY[0] = -1;
- bestXY[1] = -1;
- }
- return bestXY;
- }
-
- private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop,
- int[] direction, ItemConfiguration currentState) {
- CellAndSpan c = currentState.map.get(v);
- boolean success = false;
- mTmpOccupied.markCells(c, false);
- mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
-
- findNearestArea(c.cellX, c.cellY, c.spanX, c.spanY, direction,
- mTmpOccupied.cells, null, mTempLocation);
-
- if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
- c.cellX = mTempLocation[0];
- c.cellY = mTempLocation[1];
- success = true;
- }
- mTmpOccupied.markCells(c, true);
- return success;
- }
-
- /**
- * This helper class defines a cluster of views. It helps with defining complex edges
- * of the cluster and determining how those edges interact with other views. The edges
- * essentially define a fine-grained boundary around the cluster of views -- like a more
- * precise version of a bounding box.
- */
- private class ViewCluster {
- final static int LEFT = 1 << 0;
- final static int TOP = 1 << 1;
- final static int RIGHT = 1 << 2;
- final static int BOTTOM = 1 << 3;
-
- final ArrayList<View> views;
- final ItemConfiguration config;
- final Rect boundingRect = new Rect();
-
- final int[] leftEdge = new int[mCountY];
- final int[] rightEdge = new int[mCountY];
- final int[] topEdge = new int[mCountX];
- final int[] bottomEdge = new int[mCountX];
- int dirtyEdges;
- boolean boundingRectDirty;
-
- @SuppressWarnings("unchecked")
- public ViewCluster(ArrayList<View> views, ItemConfiguration config) {
- this.views = (ArrayList<View>) views.clone();
- this.config = config;
- resetEdges();
- }
-
- void resetEdges() {
- for (int i = 0; i < mCountX; i++) {
- topEdge[i] = -1;
- bottomEdge[i] = -1;
- }
- for (int i = 0; i < mCountY; i++) {
- leftEdge[i] = -1;
- rightEdge[i] = -1;
- }
- dirtyEdges = LEFT | TOP | RIGHT | BOTTOM;
- boundingRectDirty = true;
- }
-
- void computeEdge(int which) {
- int count = views.size();
- for (int i = 0; i < count; i++) {
- CellAndSpan cs = config.map.get(views.get(i));
- switch (which) {
- case LEFT:
- int left = cs.cellX;
- for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
- if (left < leftEdge[j] || leftEdge[j] < 0) {
- leftEdge[j] = left;
- }
- }
- break;
- case RIGHT:
- int right = cs.cellX + cs.spanX;
- for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
- if (right > rightEdge[j]) {
- rightEdge[j] = right;
- }
- }
- break;
- case TOP:
- int top = cs.cellY;
- for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
- if (top < topEdge[j] || topEdge[j] < 0) {
- topEdge[j] = top;
- }
- }
- break;
- case BOTTOM:
- int bottom = cs.cellY + cs.spanY;
- for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
- if (bottom > bottomEdge[j]) {
- bottomEdge[j] = bottom;
- }
- }
- break;
- }
- }
- }
-
- boolean isViewTouchingEdge(View v, int whichEdge) {
- CellAndSpan cs = config.map.get(v);
-
- if ((dirtyEdges & whichEdge) == whichEdge) {
- computeEdge(whichEdge);
- dirtyEdges &= ~whichEdge;
- }
-
- switch (whichEdge) {
- case LEFT:
- for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
- if (leftEdge[i] == cs.cellX + cs.spanX) {
- return true;
- }
- }
- break;
- case RIGHT:
- for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
- if (rightEdge[i] == cs.cellX) {
- return true;
- }
- }
- break;
- case TOP:
- for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
- if (topEdge[i] == cs.cellY + cs.spanY) {
- return true;
- }
- }
- break;
- case BOTTOM:
- for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
- if (bottomEdge[i] == cs.cellY) {
- return true;
- }
- }
- break;
- }
- return false;
- }
-
- void shift(int whichEdge, int delta) {
- for (View v: views) {
- CellAndSpan c = config.map.get(v);
- switch (whichEdge) {
- case LEFT:
- c.cellX -= delta;
- break;
- case RIGHT:
- c.cellX += delta;
- break;
- case TOP:
- c.cellY -= delta;
- break;
- case BOTTOM:
- default:
- c.cellY += delta;
- break;
- }
- }
- resetEdges();
- }
-
- public void addView(View v) {
- views.add(v);
- resetEdges();
- }
-
- public Rect getBoundingRect() {
- if (boundingRectDirty) {
- config.getBoundingRectForViews(views, boundingRect);
- }
- return boundingRect;
- }
-
- final PositionComparator comparator = new PositionComparator();
- class PositionComparator implements Comparator<View> {
- int whichEdge = 0;
- public int compare(View left, View right) {
- CellAndSpan l = config.map.get(left);
- CellAndSpan r = config.map.get(right);
- switch (whichEdge) {
- case LEFT:
- return (r.cellX + r.spanX) - (l.cellX + l.spanX);
- case RIGHT:
- return l.cellX - r.cellX;
- case TOP:
- return (r.cellY + r.spanY) - (l.cellY + l.spanY);
- case BOTTOM:
- default:
- return l.cellY - r.cellY;
- }
- }
- }
-
- public void sortConfigurationForEdgePush(int edge) {
- comparator.whichEdge = edge;
- Collections.sort(config.sortedViews, comparator);
- }
- }
-
- private boolean pushViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
- int[] direction, View dragView, ItemConfiguration currentState) {
-
- ViewCluster cluster = new ViewCluster(views, currentState);
- Rect clusterRect = cluster.getBoundingRect();
- int whichEdge;
- int pushDistance;
- boolean fail = false;
-
- // Determine the edge of the cluster that will be leading the push and how far
- // the cluster must be shifted.
- if (direction[0] < 0) {
- whichEdge = ViewCluster.LEFT;
- pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left;
- } else if (direction[0] > 0) {
- whichEdge = ViewCluster.RIGHT;
- pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left;
- } else if (direction[1] < 0) {
- whichEdge = ViewCluster.TOP;
- pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top;
- } else {
- whichEdge = ViewCluster.BOTTOM;
- pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top;
- }
-
- // Break early for invalid push distance.
- if (pushDistance <= 0) {
- return false;
- }
-
- // Mark the occupied state as false for the group of views we want to move.
- for (View v: views) {
- CellAndSpan c = currentState.map.get(v);
- mTmpOccupied.markCells(c, false);
- }
-
- // We save the current configuration -- if we fail to find a solution we will revert
- // to the initial state. The process of finding a solution modifies the configuration
- // in place, hence the need for revert in the failure case.
- currentState.save();
-
- // The pushing algorithm is simplified by considering the views in the order in which
- // they would be pushed by the cluster. For example, if the cluster is leading with its
- // left edge, we consider sort the views by their right edge, from right to left.
- cluster.sortConfigurationForEdgePush(whichEdge);
-
- while (pushDistance > 0 && !fail) {
- for (View v: currentState.sortedViews) {
- // For each view that isn't in the cluster, we see if the leading edge of the
- // cluster is contacting the edge of that view. If so, we add that view to the
- // cluster.
- if (!cluster.views.contains(v) && v != dragView) {
- if (cluster.isViewTouchingEdge(v, whichEdge)) {
- CellLayoutLayoutParams lp = (CellLayoutLayoutParams) v.getLayoutParams();
- if (!lp.canReorder) {
- // The push solution includes the all apps button, this is not viable.
- fail = true;
- break;
- }
- cluster.addView(v);
- CellAndSpan c = currentState.map.get(v);
-
- // Adding view to cluster, mark it as not occupied.
- mTmpOccupied.markCells(c, false);
- }
- }
- }
- pushDistance--;
-
- // The cluster has been completed, now we move the whole thing over in the appropriate
- // direction.
- cluster.shift(whichEdge, 1);
- }
-
- boolean foundSolution = false;
- clusterRect = cluster.getBoundingRect();
-
- // Due to the nature of the algorithm, the only check required to verify a valid solution
- // is to ensure that completed shifted cluster lies completely within the cell layout.
- if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCountX && clusterRect.top >= 0 &&
- clusterRect.bottom <= mCountY) {
- foundSolution = true;
- } else {
- currentState.restore();
- }
-
- // In either case, we set the occupied array as marked for the location of the views
- for (View v: cluster.views) {
- CellAndSpan c = currentState.map.get(v);
- mTmpOccupied.markCells(c, true);
- }
-
- return foundSolution;
- }
-
- private boolean addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
- int[] direction, View dragView, ItemConfiguration currentState) {
- if (views.size() == 0) return true;
-
- boolean success = false;
- Rect boundingRect = new Rect();
- // We construct a rect which represents the entire group of views passed in
- currentState.getBoundingRectForViews(views, boundingRect);
-
- // Mark the occupied state as false for the group of views we want to move.
- for (View v: views) {
- CellAndSpan c = currentState.map.get(v);
- mTmpOccupied.markCells(c, false);
- }
-
- GridOccupancy blockOccupied = new GridOccupancy(boundingRect.width(), boundingRect.height());
- int top = boundingRect.top;
- int left = boundingRect.left;
- // We mark more precisely which parts of the bounding rect are truly occupied, allowing
- // for interlocking.
- for (View v: views) {
- CellAndSpan c = currentState.map.get(v);
- blockOccupied.markCells(c.cellX - left, c.cellY - top, c.spanX, c.spanY, true);
- }
-
- mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
-
- findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(),
- boundingRect.height(), direction,
- mTmpOccupied.cells, blockOccupied.cells, mTempLocation);
-
- // If we successfuly found a location by pushing the block of views, we commit it
- if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
- int deltaX = mTempLocation[0] - boundingRect.left;
- int deltaY = mTempLocation[1] - boundingRect.top;
- for (View v: views) {
- CellAndSpan c = currentState.map.get(v);
- c.cellX += deltaX;
- c.cellY += deltaY;
- }
- success = true;
- }
-
- // In either case, we set the occupied array as marked for the location of the views
- for (View v: views) {
- CellAndSpan c = currentState.map.get(v);
- mTmpOccupied.markCells(c, true);
- }
- return success;
- }
-
- // This method tries to find a reordering solution which satisfies the push mechanic by trying
- // to push items in each of the cardinal directions, in an order based on the direction vector
- // passed.
- private boolean attemptPushInDirection(ArrayList<View> intersectingViews, Rect occupied,
- int[] direction, View ignoreView, ItemConfiguration solution) {
- if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) {
- // If the direction vector has two non-zero components, we try pushing
- // separately in each of the components.
- int temp = direction[1];
- direction[1] = 0;
-
- if (pushViewsToTempLocation(intersectingViews, occupied, direction,
- ignoreView, solution)) {
- return true;
- }
- direction[1] = temp;
- temp = direction[0];
- direction[0] = 0;
-
- if (pushViewsToTempLocation(intersectingViews, occupied, direction,
- ignoreView, solution)) {
- return true;
- }
- // Revert the direction
- direction[0] = temp;
-
- // Now we try pushing in each component of the opposite direction
- direction[0] *= -1;
- direction[1] *= -1;
- temp = direction[1];
- direction[1] = 0;
- if (pushViewsToTempLocation(intersectingViews, occupied, direction,
- ignoreView, solution)) {
- return true;
- }
-
- direction[1] = temp;
- temp = direction[0];
- direction[0] = 0;
- if (pushViewsToTempLocation(intersectingViews, occupied, direction,
- ignoreView, solution)) {
- return true;
- }
- // revert the direction
- direction[0] = temp;
- direction[0] *= -1;
- direction[1] *= -1;
-
- } else {
- // If the direction vector has a single non-zero component, we push first in the
- // direction of the vector
- if (pushViewsToTempLocation(intersectingViews, occupied, direction,
- ignoreView, solution)) {
- return true;
- }
- // Then we try the opposite direction
- direction[0] *= -1;
- direction[1] *= -1;
- if (pushViewsToTempLocation(intersectingViews, occupied, direction,
- ignoreView, solution)) {
- return true;
- }
- // Switch the direction back
- direction[0] *= -1;
- direction[1] *= -1;
-
- // If we have failed to find a push solution with the above, then we try
- // to find a solution by pushing along the perpendicular axis.
-
- // Swap the components
- int temp = direction[1];
- direction[1] = direction[0];
- direction[0] = temp;
- if (pushViewsToTempLocation(intersectingViews, occupied, direction,
- ignoreView, solution)) {
- return true;
- }
-
- // Then we try the opposite direction
- direction[0] *= -1;
- direction[1] *= -1;
- if (pushViewsToTempLocation(intersectingViews, occupied, direction,
- ignoreView, solution)) {
- return true;
- }
- // Switch the direction back
- direction[0] *= -1;
- direction[1] *= -1;
-
- // Swap the components back
- temp = direction[1];
- direction[1] = direction[0];
- direction[0] = temp;
- }
- return false;
- }
-
- private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction,
- View ignoreView, ItemConfiguration solution) {
- // Return early if get invalid cell positions
- if (cellX < 0 || cellY < 0) return false;
-
- mIntersectingViews.clear();
- mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY);
-
- // Mark the desired location of the view currently being dragged.
- if (ignoreView != null) {
- CellAndSpan c = solution.map.get(ignoreView);
- if (c != null) {
- c.cellX = cellX;
- c.cellY = cellY;
- }
- }
- Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
- Rect r1 = new Rect();
- for (View child: solution.map.keySet()) {
- if (child == ignoreView) continue;
- CellAndSpan c = solution.map.get(child);
- CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
- r1.set(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY);
- if (Rect.intersects(r0, r1)) {
- if (!lp.canReorder) {
- return false;
- }
- mIntersectingViews.add(child);
- }
- }
-
- solution.intersectingViews = new ArrayList<>(mIntersectingViews);
-
- // First we try to find a solution which respects the push mechanic. That is,
- // we try to find a solution such that no displaced item travels through another item
- // without also displacing that item.
- if (attemptPushInDirection(mIntersectingViews, mOccupiedRect, direction, ignoreView,
- solution)) {
- return true;
- }
-
- // Next we try moving the views as a block, but without requiring the push mechanic.
- if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, ignoreView,
- solution)) {
- return true;
- }
-
- // Ok, they couldn't move as a block, let's move them individually
- for (View v : mIntersectingViews) {
- if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) {
- return false;
- }
- }
- return true;
- }
-
- /*
- * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between
- * the provided point and the provided cell
- */
- private void computeDirectionVector(float deltaX, float deltaY, int[] result) {
- double angle = Math.atan(deltaY / deltaX);
-
- result[0] = 0;
- result[1] = 0;
- if (Math.abs(Math.cos(angle)) > 0.5f) {
- result[0] = (int) Math.signum(deltaX);
- }
- if (Math.abs(Math.sin(angle)) > 0.5f) {
- result[1] = (int) Math.signum(deltaY);
- }
- }
-
- private ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY,
- int spanX, int spanY, int[] direction, View dragView, boolean decX,
- ItemConfiguration solution) {
- // Copy the current state into the solution. This solution will be manipulated as necessary.
- copyCurrentStateToSolution(solution, false);
- // Copy the current occupied array into the temporary occupied array. This array will be
- // manipulated as necessary to find a solution.
- mOccupied.copyTo(mTmpOccupied);
-
- // We find the nearest cell into which we would place the dragged item, assuming there's
- // nothing in its way.
- int result[] = new int[2];
- result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
-
- boolean success;
- // First we try the exact nearest position of the item being dragged,
- // we will then want to try to move this around to other neighbouring positions
- success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView,
- solution);
-
- if (!success) {
- // We try shrinking the widget down to size in an alternating pattern, shrink 1 in
- // x, then 1 in y etc.
- if (spanX > minSpanX && (minSpanY == spanY || decX)) {
- return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY,
- direction, dragView, false, solution);
- } else if (spanY > minSpanY) {
- return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1,
- direction, dragView, true, solution);
- }
- solution.isSolution = false;
- } else {
- solution.isSolution = true;
- solution.cellX = result[0];
- solution.cellY = result[1];
- solution.spanX = spanX;
- solution.spanY = spanY;
- }
- return solution;
- }
-
- private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
- int childCount = mShortcutsAndWidgets.getChildCount();
- for (int i = 0; i < childCount; i++) {
- View child = mShortcutsAndWidgets.getChildAt(i);
- CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
- CellAndSpan c;
- if (temp) {
- c = new CellAndSpan(lp.tmpCellX, lp.tmpCellY, lp.cellHSpan, lp.cellVSpan);
- } else {
- c = new CellAndSpan(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan);
- }
- solution.add(child, c);
- }
- }
-
private void copySolutionToTempState(ItemConfiguration solution, View dragView) {
mTmpOccupied.clear();
@@ -2338,54 +1671,6 @@
return solution;
}
- /* This seems like it should be obvious and straight-forward, but when the direction vector
- needs to match with the notion of the dragView pushing other views, we have to employ
- a slightly more subtle notion of the direction vector. The question is what two points is
- the vector between? The center of the dragView and its desired destination? Not quite, as
- this doesn't necessarily coincide with the interaction of the dragView and items occupying
- those cells. Instead we use some heuristics to often lock the vector to up, down, left
- or right, which helps make pushing feel right.
- */
- private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX,
- int spanY, View dragView, int[] resultDirection) {
-
- //TODO(adamcohen) b/151776141 use the items visual center for the direction vector
- int[] targetDestination = new int[2];
-
- findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination);
- Rect dragRect = new Rect();
- cellToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
- dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY());
-
- Rect dropRegionRect = new Rect();
- getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY,
- dragView, dropRegionRect, mIntersectingViews);
-
- int dropRegionSpanX = dropRegionRect.width();
- int dropRegionSpanY = dropRegionRect.height();
-
- cellToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
- dropRegionRect.height(), dropRegionRect);
-
- int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX;
- int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY;
-
- if (dropRegionSpanX == mCountX || spanX == mCountX) {
- deltaX = 0;
- }
- if (dropRegionSpanY == mCountY || spanY == mCountY) {
- deltaY = 0;
- }
-
- if (deltaX == 0 && deltaY == 0) {
- // No idea what to do, give a random direction.
- resultDirection[0] = 1;
- resultDirection[1] = 0;
- } else {
- computeDirectionVector(deltaX, deltaY, resultDirection);
- }
- }
-
// For a given cell and span, fetch the set of views intersecting the region.
private void getViewsIntersectingRegion(int cellX, int cellY, int spanX, int spanY,
View dragView, Rect boundingRect, ArrayList<View> intersectingViews) {
@@ -2469,32 +1754,745 @@
return swapSolution.isSolution;
}
- int[] performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY,
- View dragView, int[] result, int resultSpan[], int mode) {
- // First we determine if things have moved enough to cause a different layout
+ /**
+ * Find a vacant area that will fit the given bounds nearest the requested
+ * cell location, and will also weigh in a suggested direction vector of the
+ * desired location. This method computers distance based on unit grid distances,
+ * not pixel distances.
+ *
+ * @param cellX The X cell nearest to which you want to search for a vacant area.
+ * @param cellY The Y cell nearest which you want to search for a vacant area.
+ * @param spanX Horizontal span of the object.
+ * @param spanY Vertical span of the object.
+ * @param direction The favored direction in which the views should move from x, y
+ * @param occupied The array which represents which cells in the CellLayout are occupied
+ * @param blockOccupied The array which represents which cells in the specified block (cellX,
+ * cellY, spanX, spanY) are occupied. This is used when try to move a group of views.
+ * @param result Array in which to place the result, or null (in which case a new array will
+ * be allocated)
+ * @return The X, Y cell of a vacant area that can contain this object,
+ * nearest the requested location.
+ */
+ private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction,
+ boolean[][] occupied, boolean blockOccupied[][], int[] result) {
+ // Keep track of best-scoring drop area
+ final int[] bestXY = result != null ? result : new int[2];
+ float bestDistance = Float.MAX_VALUE;
+ int bestDirectionScore = Integer.MIN_VALUE;
+
+ final int countX = mCountX;
+ final int countY = mCountY;
+
+ for (int y = 0; y < countY - (spanY - 1); y++) {
+ inner:
+ for (int x = 0; x < countX - (spanX - 1); x++) {
+ // First, let's see if this thing fits anywhere
+ for (int i = 0; i < spanX; i++) {
+ for (int j = 0; j < spanY; j++) {
+ if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) {
+ continue inner;
+ }
+ }
+ }
+
+ float distance = (float) Math.hypot(x - cellX, y - cellY);
+ int[] curDirection = mTmpPoint;
+ computeDirectionVector(x - cellX, y - cellY, curDirection);
+ // The direction score is just the dot product of the two candidate direction
+ // and that passed in.
+ int curDirectionScore = direction[0] * curDirection[0] +
+ direction[1] * curDirection[1];
+ if (Float.compare(distance, bestDistance) < 0 ||
+ (Float.compare(distance, bestDistance) == 0
+ && curDirectionScore > bestDirectionScore)) {
+ bestDistance = distance;
+ bestDirectionScore = curDirectionScore;
+ bestXY[0] = x;
+ bestXY[1] = y;
+ }
+ }
+ }
+
+ // Return -1, -1 if no suitable location found
+ if (bestDistance == Float.MAX_VALUE) {
+ bestXY[0] = -1;
+ bestXY[1] = -1;
+ }
+ return bestXY;
+ }
+
+ private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop,
+ int[] direction, ItemConfiguration currentState) {
+ CellAndSpan c = currentState.map.get(v);
+ boolean success = false;
+ mTmpOccupied.markCells(c, false);
+ mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
+
+ findNearestArea(c.cellX, c.cellY, c.spanX, c.spanY, direction,
+ mTmpOccupied.cells, null, mTempLocation);
+
+ if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
+ c.cellX = mTempLocation[0];
+ c.cellY = mTempLocation[1];
+ success = true;
+ }
+ mTmpOccupied.markCells(c, true);
+ return success;
+ }
+
+ private boolean pushViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
+ int[] direction, View dragView, ItemConfiguration currentState) {
+
+ ViewCluster cluster = new ViewCluster(views, currentState);
+ Rect clusterRect = cluster.getBoundingRect();
+ int whichEdge;
+ int pushDistance;
+ boolean fail = false;
+
+ // Determine the edge of the cluster that will be leading the push and how far
+ // the cluster must be shifted.
+ if (direction[0] < 0) {
+ whichEdge = ViewCluster.LEFT;
+ pushDistance = clusterRect.right - rectOccupiedByPotentialDrop.left;
+ } else if (direction[0] > 0) {
+ whichEdge = ViewCluster.RIGHT;
+ pushDistance = rectOccupiedByPotentialDrop.right - clusterRect.left;
+ } else if (direction[1] < 0) {
+ whichEdge = ViewCluster.TOP;
+ pushDistance = clusterRect.bottom - rectOccupiedByPotentialDrop.top;
+ } else {
+ whichEdge = ViewCluster.BOTTOM;
+ pushDistance = rectOccupiedByPotentialDrop.bottom - clusterRect.top;
+ }
+
+ // Break early for invalid push distance.
+ if (pushDistance <= 0) {
+ return false;
+ }
+
+ // Mark the occupied state as false for the group of views we want to move.
+ for (View v: views) {
+ CellAndSpan c = currentState.map.get(v);
+ mTmpOccupied.markCells(c, false);
+ }
+
+ // We save the current configuration -- if we fail to find a solution we will revert
+ // to the initial state. The process of finding a solution modifies the configuration
+ // in place, hence the need for revert in the failure case.
+ currentState.save();
+
+ // The pushing algorithm is simplified by considering the views in the order in which
+ // they would be pushed by the cluster. For example, if the cluster is leading with its
+ // left edge, we consider sort the views by their right edge, from right to left.
+ cluster.sortConfigurationForEdgePush(whichEdge);
+
+ while (pushDistance > 0 && !fail) {
+ for (View v: currentState.sortedViews) {
+ // For each view that isn't in the cluster, we see if the leading edge of the
+ // cluster is contacting the edge of that view. If so, we add that view to the
+ // cluster.
+ if (!cluster.views.contains(v) && v != dragView) {
+ if (cluster.isViewTouchingEdge(v, whichEdge)) {
+ CellLayoutLayoutParams lp = (CellLayoutLayoutParams) v.getLayoutParams();
+ if (!lp.canReorder) {
+ // The push solution includes the all apps button, this is not viable.
+ fail = true;
+ break;
+ }
+ cluster.addView(v);
+ CellAndSpan c = currentState.map.get(v);
+
+ // Adding view to cluster, mark it as not occupied.
+ mTmpOccupied.markCells(c, false);
+ }
+ }
+ }
+ pushDistance--;
+
+ // The cluster has been completed, now we move the whole thing over in the appropriate
+ // direction.
+ cluster.shift(whichEdge, 1);
+ }
+
+ boolean foundSolution = false;
+ clusterRect = cluster.getBoundingRect();
+
+ // Due to the nature of the algorithm, the only check required to verify a valid solution
+ // is to ensure that completed shifted cluster lies completely within the cell layout.
+ if (!fail && clusterRect.left >= 0 && clusterRect.right <= mCountX && clusterRect.top >= 0 &&
+ clusterRect.bottom <= mCountY) {
+ foundSolution = true;
+ } else {
+ currentState.restore();
+ }
+
+ // In either case, we set the occupied array as marked for the location of the views
+ for (View v: cluster.views) {
+ CellAndSpan c = currentState.map.get(v);
+ mTmpOccupied.markCells(c, true);
+ }
+
+ return foundSolution;
+ }
+
+ /**
+ * This helper class defines a cluster of views. It helps with defining complex edges
+ * of the cluster and determining how those edges interact with other views. The edges
+ * essentially define a fine-grained boundary around the cluster of views -- like a more
+ * precise version of a bounding box.
+ */
+ private class ViewCluster {
+ final static int LEFT = 1 << 0;
+ final static int TOP = 1 << 1;
+ final static int RIGHT = 1 << 2;
+ final static int BOTTOM = 1 << 3;
+
+ final ArrayList<View> views;
+ final ItemConfiguration config;
+ final Rect boundingRect = new Rect();
+
+ final int[] leftEdge = new int[mCountY];
+ final int[] rightEdge = new int[mCountY];
+ final int[] topEdge = new int[mCountX];
+ final int[] bottomEdge = new int[mCountX];
+ int dirtyEdges;
+ boolean boundingRectDirty;
+
+ @SuppressWarnings("unchecked")
+ public ViewCluster(ArrayList<View> views, ItemConfiguration config) {
+ this.views = (ArrayList<View>) views.clone();
+ this.config = config;
+ resetEdges();
+ }
+
+ void resetEdges() {
+ for (int i = 0; i < mCountX; i++) {
+ topEdge[i] = -1;
+ bottomEdge[i] = -1;
+ }
+ for (int i = 0; i < mCountY; i++) {
+ leftEdge[i] = -1;
+ rightEdge[i] = -1;
+ }
+ dirtyEdges = LEFT | TOP | RIGHT | BOTTOM;
+ boundingRectDirty = true;
+ }
+
+ void computeEdge(int which) {
+ int count = views.size();
+ for (int i = 0; i < count; i++) {
+ CellAndSpan cs = config.map.get(views.get(i));
+ switch (which) {
+ case LEFT:
+ int left = cs.cellX;
+ for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
+ if (left < leftEdge[j] || leftEdge[j] < 0) {
+ leftEdge[j] = left;
+ }
+ }
+ break;
+ case RIGHT:
+ int right = cs.cellX + cs.spanX;
+ for (int j = cs.cellY; j < cs.cellY + cs.spanY; j++) {
+ if (right > rightEdge[j]) {
+ rightEdge[j] = right;
+ }
+ }
+ break;
+ case TOP:
+ int top = cs.cellY;
+ for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
+ if (top < topEdge[j] || topEdge[j] < 0) {
+ topEdge[j] = top;
+ }
+ }
+ break;
+ case BOTTOM:
+ int bottom = cs.cellY + cs.spanY;
+ for (int j = cs.cellX; j < cs.cellX + cs.spanX; j++) {
+ if (bottom > bottomEdge[j]) {
+ bottomEdge[j] = bottom;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ boolean isViewTouchingEdge(View v, int whichEdge) {
+ CellAndSpan cs = config.map.get(v);
+
+ if ((dirtyEdges & whichEdge) == whichEdge) {
+ computeEdge(whichEdge);
+ dirtyEdges &= ~whichEdge;
+ }
+
+ switch (whichEdge) {
+ case LEFT:
+ for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
+ if (leftEdge[i] == cs.cellX + cs.spanX) {
+ return true;
+ }
+ }
+ break;
+ case RIGHT:
+ for (int i = cs.cellY; i < cs.cellY + cs.spanY; i++) {
+ if (rightEdge[i] == cs.cellX) {
+ return true;
+ }
+ }
+ break;
+ case TOP:
+ for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
+ if (topEdge[i] == cs.cellY + cs.spanY) {
+ return true;
+ }
+ }
+ break;
+ case BOTTOM:
+ for (int i = cs.cellX; i < cs.cellX + cs.spanX; i++) {
+ if (bottomEdge[i] == cs.cellY) {
+ return true;
+ }
+ }
+ break;
+ }
+ return false;
+ }
+
+ void shift(int whichEdge, int delta) {
+ for (View v: views) {
+ CellAndSpan c = config.map.get(v);
+ switch (whichEdge) {
+ case LEFT:
+ c.cellX -= delta;
+ break;
+ case RIGHT:
+ c.cellX += delta;
+ break;
+ case TOP:
+ c.cellY -= delta;
+ break;
+ case BOTTOM:
+ default:
+ c.cellY += delta;
+ break;
+ }
+ }
+ resetEdges();
+ }
+
+ public void addView(View v) {
+ views.add(v);
+ resetEdges();
+ }
+
+ public Rect getBoundingRect() {
+ if (boundingRectDirty) {
+ config.getBoundingRectForViews(views, boundingRect);
+ }
+ return boundingRect;
+ }
+
+ final PositionComparator comparator = new PositionComparator();
+ class PositionComparator implements Comparator<View> {
+ int whichEdge = 0;
+ public int compare(View left, View right) {
+ CellAndSpan l = config.map.get(left);
+ CellAndSpan r = config.map.get(right);
+ switch (whichEdge) {
+ case LEFT:
+ return (r.cellX + r.spanX) - (l.cellX + l.spanX);
+ case RIGHT:
+ return l.cellX - r.cellX;
+ case TOP:
+ return (r.cellY + r.spanY) - (l.cellY + l.spanY);
+ case BOTTOM:
+ default:
+ return l.cellY - r.cellY;
+ }
+ }
+ }
+
+ public void sortConfigurationForEdgePush(int edge) {
+ comparator.whichEdge = edge;
+ Collections.sort(config.sortedViews, comparator);
+ }
+ }
+
+ // This method tries to find a reordering solution which satisfies the push mechanic by trying
+ // to push items in each of the cardinal directions, in an order based on the direction vector
+ // passed.
+ private boolean attemptPushInDirection(ArrayList<View> intersectingViews, Rect occupied,
+ int[] direction, View ignoreView, ItemConfiguration solution) {
+ if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) {
+ // If the direction vector has two non-zero components, we try pushing
+ // separately in each of the components.
+ int temp = direction[1];
+ direction[1] = 0;
+
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ direction[1] = temp;
+ temp = direction[0];
+ direction[0] = 0;
+
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ // Revert the direction
+ direction[0] = temp;
+
+ // Now we try pushing in each component of the opposite direction
+ direction[0] *= -1;
+ direction[1] *= -1;
+ temp = direction[1];
+ direction[1] = 0;
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+
+ direction[1] = temp;
+ temp = direction[0];
+ direction[0] = 0;
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ // revert the direction
+ direction[0] = temp;
+ direction[0] *= -1;
+ direction[1] *= -1;
+
+ } else {
+ // If the direction vector has a single non-zero component, we push first in the
+ // direction of the vector
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ // Then we try the opposite direction
+ direction[0] *= -1;
+ direction[1] *= -1;
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ // Switch the direction back
+ direction[0] *= -1;
+ direction[1] *= -1;
+
+ // If we have failed to find a push solution with the above, then we try
+ // to find a solution by pushing along the perpendicular axis.
+
+ // Swap the components
+ int temp = direction[1];
+ direction[1] = direction[0];
+ direction[0] = temp;
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+
+ // Then we try the opposite direction
+ direction[0] *= -1;
+ direction[1] *= -1;
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction,
+ ignoreView, solution)) {
+ return true;
+ }
+ // Switch the direction back
+ direction[0] *= -1;
+ direction[1] *= -1;
+
+ // Swap the components back
+ temp = direction[1];
+ direction[1] = direction[0];
+ direction[0] = temp;
+ }
+ return false;
+ }
+
+ /*
+ * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between
+ * the provided point and the provided cell
+ */
+ private void computeDirectionVector(float deltaX, float deltaY, int[] result) {
+ double angle = Math.atan(deltaY / deltaX);
+
+ result[0] = 0;
+ result[1] = 0;
+ if (Math.abs(Math.cos(angle)) > 0.5f) {
+ result[0] = (int) Math.signum(deltaX);
+ }
+ if (Math.abs(Math.sin(angle)) > 0.5f) {
+ result[1] = (int) Math.signum(deltaY);
+ }
+ }
+
+ /* This seems like it should be obvious and straight-forward, but when the direction vector
+ needs to match with the notion of the dragView pushing other views, we have to employ
+ a slightly more subtle notion of the direction vector. The question is what two points is
+ the vector between? The center of the dragView and its desired destination? Not quite, as
+ this doesn't necessarily coincide with the interaction of the dragView and items occupying
+ those cells. Instead we use some heuristics to often lock the vector to up, down, left
+ or right, which helps make pushing feel right.
+ */
+ private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX,
+ int spanY, View dragView, int[] resultDirection) {
+
+ //TODO(adamcohen) b/151776141 use the items visual center for the direction vector
+ int[] targetDestination = new int[2];
+
+ findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination);
+ Rect dragRect = new Rect();
+ cellToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
+ dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY());
+
+ Rect dropRegionRect = new Rect();
+ getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY,
+ dragView, dropRegionRect, mIntersectingViews);
+
+ int dropRegionSpanX = dropRegionRect.width();
+ int dropRegionSpanY = dropRegionRect.height();
+
+ cellToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
+ dropRegionRect.height(), dropRegionRect);
+
+ int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX;
+ int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY;
+
+ if (dropRegionSpanX == mCountX || spanX == mCountX) {
+ deltaX = 0;
+ }
+ if (dropRegionSpanY == mCountY || spanY == mCountY) {
+ deltaY = 0;
+ }
+
+ if (deltaX == 0 && deltaY == 0) {
+ // No idea what to do, give a random direction.
+ resultDirection[0] = 1;
+ resultDirection[1] = 0;
+ } else {
+ computeDirectionVector(deltaX, deltaY, resultDirection);
+ }
+ }
+
+ private boolean addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop,
+ int[] direction, View dragView, ItemConfiguration currentState) {
+ if (views.size() == 0) return true;
+
+ boolean success = false;
+ Rect boundingRect = new Rect();
+ // We construct a rect which represents the entire group of views passed in
+ currentState.getBoundingRectForViews(views, boundingRect);
+
+ // Mark the occupied state as false for the group of views we want to move.
+ for (View v: views) {
+ CellAndSpan c = currentState.map.get(v);
+ mTmpOccupied.markCells(c, false);
+ }
+
+ GridOccupancy blockOccupied = new GridOccupancy(boundingRect.width(), boundingRect.height());
+ int top = boundingRect.top;
+ int left = boundingRect.left;
+ // We mark more precisely which parts of the bounding rect are truly occupied, allowing
+ // for interlocking.
+ for (View v: views) {
+ CellAndSpan c = currentState.map.get(v);
+ blockOccupied.markCells(c.cellX - left, c.cellY - top, c.spanX, c.spanY, true);
+ }
+
+ mTmpOccupied.markCells(rectOccupiedByPotentialDrop, true);
+
+ findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(),
+ boundingRect.height(), direction,
+ mTmpOccupied.cells, blockOccupied.cells, mTempLocation);
+
+ // If we successfully found a location by pushing the block of views, we commit it
+ if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) {
+ int deltaX = mTempLocation[0] - boundingRect.left;
+ int deltaY = mTempLocation[1] - boundingRect.top;
+ for (View v: views) {
+ CellAndSpan c = currentState.map.get(v);
+ c.cellX += deltaX;
+ c.cellY += deltaY;
+ }
+ success = true;
+ }
+
+ // In either case, we set the occupied array as marked for the location of the views
+ for (View v: views) {
+ CellAndSpan c = currentState.map.get(v);
+ mTmpOccupied.markCells(c, true);
+ }
+ return success;
+ }
+
+ private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction,
+ View ignoreView, ItemConfiguration solution) {
+ // Return early if get invalid cell positions
+ if (cellX < 0 || cellY < 0) return false;
+
+ mIntersectingViews.clear();
+ mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY);
+
+ // Mark the desired location of the view currently being dragged.
+ if (ignoreView != null) {
+ CellAndSpan c = solution.map.get(ignoreView);
+ if (c != null) {
+ c.cellX = cellX;
+ c.cellY = cellY;
+ }
+ }
+ Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
+ Rect r1 = new Rect();
+ for (View child: solution.map.keySet()) {
+ if (child == ignoreView) continue;
+ CellAndSpan c = solution.map.get(child);
+ CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
+ r1.set(c.cellX, c.cellY, c.cellX + c.spanX, c.cellY + c.spanY);
+ if (Rect.intersects(r0, r1)) {
+ if (!lp.canReorder) {
+ return false;
+ }
+ mIntersectingViews.add(child);
+ }
+ }
+
+ solution.intersectingViews = new ArrayList<>(mIntersectingViews);
+
+ // First we try to find a solution which respects the push mechanic. That is,
+ // we try to find a solution such that no displaced item travels through another item
+ // without also displacing that item.
+ if (attemptPushInDirection(mIntersectingViews, mOccupiedRect, direction, ignoreView,
+ solution)) {
+ return true;
+ }
+
+ // Next we try moving the views as a block, but without requiring the push mechanic.
+ if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, ignoreView,
+ solution)) {
+ return true;
+ }
+
+ // Ok, they couldn't move as a block, let's move them individually
+ for (View v : mIntersectingViews) {
+ if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY,
+ int spanX, int spanY, int[] direction, View dragView, boolean decX,
+ ItemConfiguration solution) {
+ // Copy the current state into the solution. This solution will be manipulated as necessary.
+ copyCurrentStateToSolution(solution, false);
+ // Copy the current occupied array into the temporary occupied array. This array will be
+ // manipulated as necessary to find a solution.
+ mOccupied.copyTo(mTmpOccupied);
+
+ // We find the nearest cell into which we would place the dragged item, assuming there's
+ // nothing in its way.
+ int result[] = new int[2];
result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
- if (resultSpan == null) {
- resultSpan = new int[2];
- }
+ boolean success;
+ // First we try the exact nearest position of the item being dragged,
+ // we will then want to try to move this around to other neighbouring positions
+ success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView,
+ solution);
- // When we are checking drop validity or actually dropping, we don't recompute the
- // direction vector, since we want the solution to match the preview, and it's possible
- // that the exact position of the item has changed to result in a new reordering outcome.
- if ((mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL || mode == MODE_ACCEPT_DROP)
- && mPreviousReorderDirection[0] != INVALID_DIRECTION) {
- mDirectionVector[0] = mPreviousReorderDirection[0];
- mDirectionVector[1] = mPreviousReorderDirection[1];
- // We reset this vector after drop
- if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
- mPreviousReorderDirection[0] = INVALID_DIRECTION;
- mPreviousReorderDirection[1] = INVALID_DIRECTION;
+ if (!success) {
+ // We try shrinking the widget down to size in an alternating pattern, shrink 1 in
+ // x, then 1 in y etc.
+ if (spanX > minSpanX && (minSpanY == spanY || decX)) {
+ return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY,
+ direction, dragView, false, solution);
+ } else if (spanY > minSpanY) {
+ return findReorderSolution(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1,
+ direction, dragView, true, solution);
}
+ solution.isSolution = false;
} else {
- getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mDirectionVector);
- mPreviousReorderDirection[0] = mDirectionVector[0];
- mPreviousReorderDirection[1] = mDirectionVector[1];
+ solution.isSolution = true;
+ solution.cellX = result[0];
+ solution.cellY = result[1];
+ solution.spanX = spanX;
+ solution.spanY = spanY;
}
+ return solution;
+ }
+
+ private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) {
+ int childCount = mShortcutsAndWidgets.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = mShortcutsAndWidgets.getChildAt(i);
+ CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
+ CellAndSpan c;
+ if (temp) {
+ c = new CellAndSpan(lp.tmpCellX, lp.tmpCellY, lp.cellHSpan, lp.cellVSpan);
+ } else {
+ c = new CellAndSpan(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan);
+ }
+ solution.add(child, c);
+ }
+ }
+
+ /**
+ * Returns a "reorder" where we simply drop the item in the closest empty space, without moving
+ * any other item in the way.
+ *
+ * @param pixelX X coordinate in pixels in the screen
+ * @param pixelY Y coordinate in pixels in the screen
+ * @param spanX horizontal cell span
+ * @param spanY vertical cell span
+ * @return the configuration that represents the found reorder
+ */
+ public ItemConfiguration closestEmptySpaceReorder(int pixelX, int pixelY, int spanX,
+ int spanY) {
+ int[] result = new int[2];
+ result = findNearestArea(pixelX, pixelY, spanX, spanY, result);
+ ItemConfiguration solution = new ItemConfiguration();
+ copyCurrentStateToSolution(solution, false);
+ solution.isSolution = result[0] != -1;
+ if (!solution.isSolution) {
+ return solution;
+ }
+ solution.cellX = result[0];
+ solution.cellY = result[1];
+ solution.spanX = spanX;
+ solution.spanY = spanY;
+ return solution;
+ }
+
+ /**
+ * When the user drags an Item in the workspace sometimes we need to move the items already in
+ * the workspace to make space for the new item, this function return a solution for that
+ * reorder.
+ *
+ * @param pixelX X coordinate in the screen of the dragView in pixels
+ * @param pixelY Y coordinate in the screen of the dragView in pixels
+ * @param minSpanX minimum horizontal span the item can be shrunk to
+ * @param minSpanY minimum vertical span the item can be shrunk to
+ * @param spanX occupied horizontal span
+ * @param spanY occupied vertical span
+ * @param dragView the view of the item being draged
+ * @return returns a solution for the given parameters, the solution contains all the icons and
+ * the locations they should be in the given solution.
+ */
+ public ItemConfiguration calculateReorder(int pixelX, int pixelY, int minSpanX, int minSpanY,
+ int spanX, int spanY, View dragView) {
+ getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mDirectionVector);
+
+ ItemConfiguration closestSpaceSolution = closestEmptySpaceReorder(pixelX, pixelY, spanX,
+ spanY);
// Find a solution involving pushing / displacing any items in the way
ItemConfiguration swapSolution = findReorderSolution(pixelX, pixelY, minSpanX, minSpanY,
@@ -2504,73 +2502,104 @@
ItemConfiguration noShuffleSolution = findConfigurationNoShuffle(pixelX, pixelY, minSpanX,
minSpanY, spanX, spanY, dragView, new ItemConfiguration());
- ItemConfiguration finalSolution = null;
-
// If the reorder solution requires resizing (shrinking) the item being dropped, we instead
// favor a solution in which the item is not resized, but
if (swapSolution.isSolution && swapSolution.area() >= noShuffleSolution.area()) {
- finalSolution = swapSolution;
+ return swapSolution;
} else if (noShuffleSolution.isSolution) {
- finalSolution = noShuffleSolution;
+ return noShuffleSolution;
+ } else if (closestSpaceSolution.isSolution) {
+ return closestSpaceSolution;
+ }
+ return null;
+ }
+
+ int[] performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY,
+ View dragView, int[] result, int[] resultSpan, int mode) {
+ if (resultSpan == null) {
+ resultSpan = new int[]{-1, -1};
+ }
+ if (result == null) {
+ result = new int[]{-1, -1};
}
- if (mode == MODE_SHOW_REORDER_HINT) {
- if (finalSolution != null) {
- beginOrAdjustReorderPreviewAnimations(finalSolution, dragView,
- ReorderPreviewAnimation.MODE_HINT);
- result[0] = finalSolution.cellX;
- result[1] = finalSolution.cellY;
- resultSpan[0] = finalSolution.spanX;
- resultSpan[1] = finalSolution.spanY;
- } else {
- result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
+ ItemConfiguration finalSolution = null;
+ // We want the solution to match the animation of the preview and to match the drop so we
+ // only recalculate in mode MODE_SHOW_REORDER_HINT because that the first one to run in the
+ // reorder cycle.
+ if (mode == MODE_SHOW_REORDER_HINT || mPreviousSolution == null) {
+ finalSolution = calculateReorder(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY,
+ dragView);
+ mPreviousSolution = finalSolution;
+ } else {
+ finalSolution = mPreviousSolution;
+ // We reset this vector after drop
+ if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
+ mPreviousSolution = null;
}
- return result;
}
- boolean foundSolution = true;
- if (!DESTRUCTIVE_REORDER) {
- setUseTempCoords(true);
- }
-
- if (finalSolution != null) {
+ if (finalSolution == null || !finalSolution.isSolution) {
+ result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
+ } else {
result[0] = finalSolution.cellX;
result[1] = finalSolution.cellY;
resultSpan[0] = finalSolution.spanX;
resultSpan[1] = finalSolution.spanY;
+ }
+ performReorder(finalSolution, dragView, mode);
+ return result;
+ }
- // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
- // committing anything or animating anything as we just want to determine if a solution
- // exists
- if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
- if (!DESTRUCTIVE_REORDER) {
- copySolutionToTempState(finalSolution, dragView);
- }
- setItemPlacementDirty(true);
- animateItemsToSolution(finalSolution, dragView, mode == MODE_ON_DROP);
-
- if (!DESTRUCTIVE_REORDER &&
- (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) {
- // Since the temp solution didn't update dragView, don't commit it either
- commitTempPlacement(dragView);
- completeAndClearReorderPreviewAnimations();
- setItemPlacementDirty(false);
- } else {
- beginOrAdjustReorderPreviewAnimations(finalSolution, dragView,
- ReorderPreviewAnimation.MODE_PREVIEW);
- }
+ /**
+ * Animates and submits in the DB the given ItemConfiguration depending of the mode.
+ *
+ * @param solution represents widgets on the screen which the Workspace will animate to and
+ * would be submitted to the database.
+ * @param dragView view which is being dragged over the workspace that trigger the reorder
+ * @param mode depending on the mode different animations would be played and depending on the
+ * mode the solution would be submitted or not the database.
+ * The possible modes are {@link MODE_SHOW_REORDER_HINT}, {@link MODE_DRAG_OVER},
+ * {@link MODE_ON_DROP}, {@link MODE_ON_DROP_EXTERNAL}, {@link MODE_ACCEPT_DROP}
+ * defined in {@link CellLayout}.
+ */
+ void performReorder(ItemConfiguration solution, View dragView, int mode) {
+ if (mode == MODE_SHOW_REORDER_HINT) {
+ beginOrAdjustReorderPreviewAnimations(solution, dragView,
+ ReorderPreviewAnimation.MODE_HINT);
+ return;
+ }
+ // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother
+ // committing anything or animating anything as we just want to determine if a solution
+ // exists
+ if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
+ if (!DESTRUCTIVE_REORDER) {
+ setUseTempCoords(true);
}
- } else {
- foundSolution = false;
- result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1;
+
+ if (!DESTRUCTIVE_REORDER) {
+ copySolutionToTempState(solution, dragView);
+ }
+ setItemPlacementDirty(true);
+ animateItemsToSolution(solution, dragView, mode == MODE_ON_DROP);
+
+ if (!DESTRUCTIVE_REORDER
+ && (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) {
+ // Since the temp solution didn't update dragView, don't commit it either
+ commitTempPlacement(dragView);
+ completeAndClearReorderPreviewAnimations();
+ setItemPlacementDirty(false);
+ } else {
+ beginOrAdjustReorderPreviewAnimations(solution, dragView,
+ ReorderPreviewAnimation.MODE_PREVIEW);
+ }
}
- if ((mode == MODE_ON_DROP || !foundSolution) && !DESTRUCTIVE_REORDER) {
+ if (mode == MODE_ON_DROP && !DESTRUCTIVE_REORDER) {
setUseTempCoords(false);
}
mShortcutsAndWidgets.requestLayout();
- return result;
}
void setItemPlacementDirty(boolean dirty) {
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index edd809c..c91e3eb 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -25,12 +25,14 @@
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
import static com.android.launcher3.icons.GraphicsUtils.getShapePath;
+import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
import static com.android.launcher3.testing.shared.ResourceUtils.pxFromDp;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -61,6 +63,7 @@
private static final int DEFAULT_DOT_SIZE = 100;
private static final float ALL_APPS_TABLET_MAX_ROWS = 5.5f;
+ private static final float MIN_FOLDER_TEXT_SIZE_SP = 16f;
public static final PointF DEFAULT_SCALE = new PointF(1.0f, 1.0f);
public static final ViewScaleProvider DEFAULT_PROVIDER = itemInfo -> DEFAULT_SCALE;
@@ -147,11 +150,12 @@
// Folder
public float folderLabelTextScale;
public int folderLabelTextSizePx;
+ public int folderFooterHeightPx;
public int folderIconSizePx;
public int folderIconOffsetYPx;
// Folder content
- public Point folderCellLayoutBorderSpacePx;
+ public int folderCellLayoutBorderSpacePx;
public int folderContentPaddingLeftRight;
public int folderContentPaddingTop;
@@ -312,10 +316,14 @@
}
if (isTaskbarPresent) {
- taskbarSize = DisplayController.isTransientTaskbar(context)
- ? res.getDimensionPixelSize(R.dimen.transient_taskbar_size)
- : res.getDimensionPixelSize(R.dimen.taskbar_size);
- stashedTaskbarSize = res.getDimensionPixelSize(R.dimen.taskbar_stashed_size);
+ if (DisplayController.isTransientTaskbar(context)) {
+ taskbarSize = res.getDimensionPixelSize(R.dimen.transient_taskbar_size);
+ stashedTaskbarSize =
+ res.getDimensionPixelSize(R.dimen.transient_taskbar_stashed_size);
+ } else {
+ taskbarSize = res.getDimensionPixelSize(R.dimen.taskbar_size);
+ stashedTaskbarSize = res.getDimensionPixelSize(R.dimen.taskbar_stashed_size);
+ }
}
edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
@@ -348,17 +356,34 @@
}
folderLabelTextScale = res.getFloat(R.dimen.folder_label_text_scale);
- folderContentPaddingLeftRight =
- res.getDimensionPixelSize(R.dimen.folder_content_padding_left_right);
- folderContentPaddingTop = res.getDimensionPixelSize(R.dimen.folder_content_padding_top);
+
+ if (inv.folderStyle != INVALID_RESOURCE_HANDLE) {
+ TypedArray folderStyle = context.obtainStyledAttributes(inv.folderStyle,
+ R.styleable.FolderDisplayStyle);
+ // These are re-set in #updateFolderCellSize if the grid is not scalable
+ folderCellHeightPx = folderStyle.getDimensionPixelSize(
+ R.styleable.FolderDisplayStyle_folderCellHeight, 0);
+ folderCellWidthPx = folderStyle.getDimensionPixelSize(
+ R.styleable.FolderDisplayStyle_folderCellWidth, 0);
+
+ folderContentPaddingTop = folderStyle.getDimensionPixelSize(
+ R.styleable.FolderDisplayStyle_folderTopPadding, 0);
+ folderCellLayoutBorderSpacePx = folderStyle.getDimensionPixelSize(
+ R.styleable.FolderDisplayStyle_folderBorderSpace, 0);
+ folderFooterHeightPx = folderStyle.getDimensionPixelSize(
+ R.styleable.FolderDisplayStyle_folderFooterHeight, 0);
+ folderStyle.recycle();
+ } else {
+ folderCellLayoutBorderSpacePx = 0;
+ folderFooterHeightPx = 0;
+ folderContentPaddingTop = res.getDimensionPixelSize(R.dimen.folder_top_padding_default);
+ }
cellLayoutBorderSpacePx = getCellLayoutBorderSpace(inv);
+ cellLayoutBorderSpaceOriginalPx = new Point(cellLayoutBorderSpacePx);
allAppsBorderSpacePx = new Point(
pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].x, mMetrics),
pxFromDp(inv.allAppsBorderSpaces[mTypeIndex].y, mMetrics));
- cellLayoutBorderSpaceOriginalPx = new Point(cellLayoutBorderSpacePx);
- folderCellLayoutBorderSpacePx = new Point(pxFromDp(inv.folderBorderSpaces.x, mMetrics),
- pxFromDp(inv.folderBorderSpaces.y, mMetrics));
workspacePageIndicatorHeight = res.getDimensionPixelSize(
R.dimen.workspace_page_indicator_height);
@@ -927,22 +952,20 @@
private void updateAvailableFolderCellDimensions(Resources res) {
updateFolderCellSize(1f, res);
- final int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_height);
-
// Don't let the folder get too close to the edges of the screen.
int folderMargin = edgeMarginPx * 2;
Point totalWorkspacePadding = getTotalWorkspacePadding();
// Check if the icons fit within the available height.
float contentUsedHeight = folderCellHeightPx * inv.numFolderRows
- + ((inv.numFolderRows - 1) * folderCellLayoutBorderSpacePx.y);
- int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y - folderBottomPanelSize
+ + ((inv.numFolderRows - 1) * folderCellLayoutBorderSpacePx);
+ int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y - folderFooterHeightPx
- folderMargin - folderContentPaddingTop;
float scaleY = contentMaxHeight / contentUsedHeight;
// Check if the icons fit within the available width.
float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns
- + ((inv.numFolderColumns - 1) * folderCellLayoutBorderSpacePx.x);
+ + ((inv.numFolderColumns - 1) * folderCellLayoutBorderSpacePx);
int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin
- folderContentPaddingLeftRight * 2;
float scaleX = contentMaxWidth / contentUsedWidth;
@@ -960,19 +983,18 @@
folderChildIconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mMetrics, scale));
folderChildTextSizePx =
pxFromSp(inv.iconTextSize[INDEX_DEFAULT], mMetrics, scale);
- folderLabelTextSizePx = (int) (folderChildTextSizePx * folderLabelTextScale);
+ folderLabelTextSizePx = Math.max(pxFromSp(MIN_FOLDER_TEXT_SIZE_SP, mMetrics),
+ (int) (folderChildTextSizePx * folderLabelTextScale));
int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx);
if (isScalableGrid) {
- folderCellWidthPx = pxFromDp(inv.folderCellSize.x, mMetrics, scale);
- folderCellHeightPx = pxFromDp(inv.folderCellSize.y, mMetrics, scale);
+ if (inv.folderStyle == INVALID_RESOURCE_HANDLE) {
+ folderCellWidthPx = pxFromDp(getCellSize().x, mMetrics, scale);
+ folderCellHeightPx = pxFromDp(getCellSize().y, mMetrics, scale);
+ }
- folderCellLayoutBorderSpacePx = new Point(
- pxFromDp(inv.folderBorderSpaces.x, mMetrics, scale),
- pxFromDp(inv.folderBorderSpaces.y, mMetrics, scale));
- folderContentPaddingLeftRight = folderCellLayoutBorderSpacePx.x;
- folderContentPaddingTop = pxFromDp(inv.folderTopPadding, mMetrics, scale);
+ folderContentPaddingLeftRight = folderCellLayoutBorderSpacePx;
} else {
int cellPaddingX = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding)
* scale);
@@ -981,6 +1003,10 @@
folderCellWidthPx = folderChildIconSizePx + 2 * cellPaddingX;
folderCellHeightPx = folderChildIconSizePx + 2 * cellPaddingY + textHeight;
+ folderContentPaddingLeftRight =
+ res.getDimensionPixelSize(R.dimen.folder_content_padding_left_right);
+ folderFooterHeightPx =
+ res.getDimensionPixelSize(R.dimen.folder_footer_height_default);
}
folderChildDrawablePaddingPx = Math.max(0,
@@ -1464,13 +1490,12 @@
writer.println(prefix + pxToDpStr("folderChildTextSizePx", folderChildTextSizePx));
writer.println(prefix + pxToDpStr("folderChildDrawablePaddingPx",
folderChildDrawablePaddingPx));
- writer.println(prefix + pxToDpStr("folderCellLayoutBorderSpacePx Horizontal",
- folderCellLayoutBorderSpacePx.x));
- writer.println(prefix + pxToDpStr("folderCellLayoutBorderSpacePx Vertical",
- folderCellLayoutBorderSpacePx.y));
+ writer.println(prefix + pxToDpStr("folderCellLayoutBorderSpacePx",
+ folderCellLayoutBorderSpacePx));
writer.println(prefix + pxToDpStr("folderContentPaddingLeftRight",
folderContentPaddingLeftRight));
writer.println(prefix + pxToDpStr("folderTopPadding", folderContentPaddingTop));
+ writer.println(prefix + pxToDpStr("folderFooterHeight", folderFooterHeightPx));
writer.println(prefix + pxToDpStr("bottomSheetTopPadding", bottomSheetTopPadding));
writer.println(prefix + "\tbottomSheetOpenDuration: " + bottomSheetOpenDuration);
diff --git a/src/com/android/launcher3/DropTargetBar.java b/src/com/android/launcher3/DropTargetBar.java
index 98ecf3a..5225731 100644
--- a/src/com/android/launcher3/DropTargetBar.java
+++ b/src/com/android/launcher3/DropTargetBar.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.ButtonDropTarget.TOOLTIP_DEFAULT;
import static com.android.launcher3.anim.AlphaUpdateListener.updateVisibility;
+import static com.android.launcher3.config.FeatureFlags.HOME_GARDENING_WORKSPACE_BUTTONS;
import android.animation.TimeInterpolator;
import android.content.Context;
@@ -118,7 +119,13 @@
lp.rightMargin = (grid.widthPx - lp.width) / 2;
}
lp.height = grid.dropTargetBarSizePx;
- lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+ // TODO: Add tablet support for DropTargetBar when HOME_GARDENING_WORKSPACE_BUTTONS flag
+ // is on
+ if (HOME_GARDENING_WORKSPACE_BUTTONS.get()) {
+ lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
+ } else {
+ lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+ }
DeviceProfile dp = mLauncher.getDeviceProfile();
int horizontalPadding = dp.dropTargetHorizontalPaddingPx;
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index a8d371e..0c6f340 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.Utilities.dpiFromPx;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TWO_PANEL_HOME;
+import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS;
@@ -43,8 +44,10 @@
import android.util.Xml;
import android.view.Display;
+import androidx.annotation.DimenRes;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.ResourcesCompat;
@@ -64,6 +67,8 @@
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -127,11 +132,9 @@
public PointF[] minCellSize;
public PointF[] borderSpaces;
- public int inlineNavButtonsEndSpacing;
+ public @DimenRes int inlineNavButtonsEndSpacing;
- public PointF folderBorderSpaces;
- public PointF folderCellSize;
- public float folderTopPadding;
+ public @StyleRes int folderStyle;
public float[] horizontalMargin;
@@ -331,8 +334,11 @@
dbFile = closestProfile.dbFile;
defaultLayoutId = closestProfile.defaultLayoutId;
demoModeLayoutId = closestProfile.demoModeLayoutId;
+
numFolderRows = closestProfile.numFolderRows;
numFolderColumns = closestProfile.numFolderColumns;
+ folderStyle = closestProfile.folderStyle;
+
isScalable = closestProfile.isScalable;
devicePaddingId = closestProfile.devicePaddingId;
this.deviceType = deviceType;
@@ -355,10 +361,6 @@
borderSpaces = displayOption.borderSpaces;
- folderBorderSpaces = displayOption.folderBorderSpaces;
- folderCellSize = displayOption.folderCellSize;
- folderTopPadding = displayOption.folderTopPadding;
-
horizontalMargin = displayOption.horizontalMargin;
numShownHotseatIcons = closestProfile.numHotseatIcons;
@@ -641,6 +643,18 @@
float screenHeight = config.screenHeightDp * res.getDisplayMetrics().density;
int rotation = WindowManagerProxy.INSTANCE.get(context).getRotation(context);
+ if (Utilities.IS_DEBUG_DEVICE) {
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stringWriter);
+ DisplayController.INSTANCE.get(context).dump(printWriter);
+ printWriter.flush();
+ Log.d("b/253338238", "getDeviceProfile -"
+ + "\nconfig: " + config
+ + "\ndisplayMetrics: " + res.getDisplayMetrics()
+ + "\nrotation: " + rotation
+ + "\n" + stringWriter,
+ new Exception());
+ }
return getBestMatch(screenWidth, screenHeight, rotation);
}
@@ -735,6 +749,7 @@
private final int numFolderRows;
private final int numFolderColumns;
+ private final @StyleRes int folderStyle;
private final int numAllAppsColumns;
private final int numDatabaseAllAppsColumns;
@@ -745,7 +760,7 @@
private final boolean[] inlineQsb = new boolean[COUNT_SIZES];
- private int inlineNavButtonsEndSpacing;
+ private @DimenRes int inlineNavButtonsEndSpacing;
private final String dbFile;
private final int defaultLayoutId;
@@ -797,11 +812,15 @@
inlineNavButtonsEndSpacing =
a.getResourceId(R.styleable.GridDisplayOption_inlineNavButtonsEndSpacing,
R.dimen.taskbar_button_margin_default);
+
numFolderRows = a.getInt(
R.styleable.GridDisplayOption_numFolderRows, numRows);
numFolderColumns = a.getInt(
R.styleable.GridDisplayOption_numFolderColumns, numColumns);
+ folderStyle = a.getResourceId(R.styleable.GridDisplayOption_folderStyle,
+ INVALID_RESOURCE_HANDLE);
+
isScalable = a.getBoolean(
R.styleable.GridDisplayOption_isScalable, false);
devicePaddingId = a.getResourceId(
@@ -846,10 +865,6 @@
private final PointF[] minCellSize = new PointF[COUNT_SIZES];
- private final PointF folderCellSize;
- private final PointF folderBorderSpaces;
- private float folderTopPadding;
-
private final PointF[] borderSpaces = new PointF[COUNT_SIZES];
private final float[] horizontalMargin = new float[COUNT_SIZES];
private final float[] hotseatBarBottomSpace = new float[COUNT_SIZES];
@@ -932,21 +947,6 @@
borderSpaceTwoPanelLandscape);
borderSpaces[INDEX_TWO_PANEL_LANDSCAPE] = new PointF(x, y);
- x = a.getFloat(R.styleable.ProfileDisplayOption_folderCellWidth,
- minCellSize[INDEX_DEFAULT].x);
- y = a.getFloat(R.styleable.ProfileDisplayOption_folderCellHeight,
- minCellSize[INDEX_DEFAULT].y);
- folderCellSize = new PointF(x, y);
-
- float folderBorderSpace = a.getFloat(R.styleable.ProfileDisplayOption_folderBorderSpace,
- borderSpace);
-
- x = y = folderBorderSpace;
- folderBorderSpaces = new PointF(x, y);
-
- folderTopPadding = a.getFloat(R.styleable.ProfileDisplayOption_folderTopPadding,
- folderBorderSpaces.y);
-
x = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellWidth,
minCellSize[INDEX_DEFAULT].x);
y = a.getFloat(R.styleable.ProfileDisplayOption_allAppsCellHeight,
@@ -1119,9 +1119,6 @@
allAppsIconTextSizes[i] = 0;
allAppsBorderSpaces[i] = new PointF();
}
- folderBorderSpaces = new PointF();
- folderCellSize = new PointF();
- folderTopPadding = 0f;
}
private DisplayOption multiply(float w) {
@@ -1142,11 +1139,6 @@
allAppsBorderSpaces[i].x *= w;
allAppsBorderSpaces[i].y *= w;
}
- folderBorderSpaces.x *= w;
- folderBorderSpaces.y *= w;
- folderCellSize.x *= w;
- folderCellSize.y *= w;
- folderTopPadding *= w;
return this;
}
@@ -1169,11 +1161,6 @@
allAppsBorderSpaces[i].x += p.allAppsBorderSpaces[i].x;
allAppsBorderSpaces[i].y += p.allAppsBorderSpaces[i].y;
}
- folderBorderSpaces.x += p.folderBorderSpaces.x;
- folderBorderSpaces.y += p.folderBorderSpaces.y;
- folderCellSize.x += p.folderCellSize.x;
- folderCellSize.y += p.folderCellSize.y;
- folderTopPadding += p.folderTopPadding;
return this;
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 7269d14..ab7f622 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -44,6 +44,7 @@
import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions;
import static com.android.launcher3.anim.Interpolators.EMPHASIZED;
import static com.android.launcher3.config.FeatureFlags.SHOW_DELIGHTFUL_PAGINATION;
+import static com.android.launcher3.config.FeatureFlags.SHOW_DOT_PAGINATION;
import static com.android.launcher3.logging.StatsLogManager.EventEnum;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
@@ -52,6 +53,8 @@
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_EXIT;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONSTOP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RECONFIGURED;
import static com.android.launcher3.model.ItemInstallQueue.FLAG_ACTIVITY_PAUSED;
import static com.android.launcher3.model.ItemInstallQueue.FLAG_DRAG_AND_DROP;
@@ -142,6 +145,7 @@
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.dragndrop.DragView;
import com.android.launcher3.dragndrop.LauncherDragController;
+import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderGridOrganizer;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.icons.BitmapRenderer;
@@ -181,7 +185,6 @@
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.touch.AllAppsSwipeController;
-import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.launcher3.util.ActivityResultInfo;
import com.android.launcher3.util.ActivityTracker;
@@ -220,7 +223,6 @@
import com.android.systemui.plugins.shared.LauncherExterns;
import com.android.systemui.plugins.shared.LauncherOverlayManager;
import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
-import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -240,7 +242,7 @@
*/
public class Launcher extends StatefulActivity<LauncherState>
implements LauncherExterns, Callbacks, InvariantDeviceProfile.OnIDPChangeListener,
- PluginListener<LauncherOverlayPlugin>, LauncherOverlayCallbacks {
+ PluginListener<LauncherOverlayPlugin> {
public static final String TAG = "Launcher";
public static final ActivityTracker<Launcher> ACTIVITY_TRACKER = new ActivityTracker<>();
@@ -480,6 +482,9 @@
mOnboardingPrefs = createOnboardingPrefs(mSharedPrefs);
+ // TODO: move the SearchConfig to SearchState when new LauncherState is created.
+ mBaseSearchConfig = new BaseSearchConfig();
+
mAppWidgetManager = new WidgetManagerHelper(this);
mAppWidgetHolder = createAppWidgetHolder();
mAppWidgetHolder.startListening();
@@ -546,9 +551,6 @@
getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
}
setTitle(R.string.home_screen);
-
- // TODO: move the SearchConfig to SearchState when new LauncherState is created.
- mBaseSearchConfig = new BaseSearchConfig();
}
protected LauncherOverlayManager getDefaultOverlay() {
@@ -694,17 +696,9 @@
*/
@Override
public void setLauncherOverlay(LauncherOverlay overlay) {
- if (overlay != null) {
- overlay.setOverlayCallbacks(this);
- }
mWorkspace.setLauncherOverlay(overlay);
}
- @Override
- public void runOnOverlayHidden(Runnable runnable) {
- getWorkspace().runOnOverlayHidden(runnable);
- }
-
public boolean setLauncherCallbacks(LauncherCallbacks callbacks) {
mLauncherCallbacks = callbacks;
return true;
@@ -1191,7 +1185,6 @@
mOverlayManager.onActivityResumed(this);
}
- AbstractFloatingView.closeAllOpenViewsExcept(this, false, TYPE_REBIND_SAFE);
DragView.removeAllViews(this);
TraceHelper.INSTANCE.endSection(traceToken);
}
@@ -1213,18 +1206,6 @@
}
/**
- * {@code LauncherOverlayCallbacks} scroll amount.
- * Indicates transition progress to -1 screen.
- * @param progress From 0 to 1.
- */
- @Override
- public void onScrollChanged(float progress) {
- if (mWorkspace != null) {
- mWorkspace.onOverlayScrollChanged(progress);
- }
- }
-
- /**
* Restores the previous state, if it exists.
*
* @param savedState The previous state.
@@ -1300,7 +1281,7 @@
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
- if (SHOW_DELIGHTFUL_PAGINATION.get()
+ if ((SHOW_DOT_PAGINATION.get() || SHOW_DELIGHTFUL_PAGINATION.get())
&& WorkspacePageIndicator.class.getName().equals(name)) {
return LayoutInflater.from(context).inflate(R.layout.page_indicator_dots,
(ViewGroup) parent, false);
@@ -1331,7 +1312,7 @@
BubbleTextView favorite = (BubbleTextView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.app_icon, parent, false);
favorite.applyFromWorkspaceItem(info);
- favorite.setOnClickListener(ItemClickHandler.INSTANCE);
+ favorite.setOnClickListener(getItemOnClickListener());
favorite.setOnFocusChangeListener(mFocusHandler);
return favorite;
}
@@ -1617,7 +1598,6 @@
&& AbstractFloatingView.getTopOpenView(this) == null;
boolean isActionMain = Intent.ACTION_MAIN.equals(intent.getAction());
boolean internalStateHandled = ACTIVITY_TRACKER.handleNewIntent(this);
- hideKeyboard();
if (isActionMain) {
if (!internalStateHandled) {
@@ -1704,6 +1684,10 @@
outState.remove(RUNTIME_STATE_WIDGET_PANEL);
}
+ // We close any open folders and shortcut containers that are not safe for rebind,
+ // and we need to make sure this state is reflected.
+ AbstractFloatingView.closeAllOpenViewsExcept(
+ this, isStarted() && !isForceInvisible(), TYPE_REBIND_SAFE);
finishAutoCancelActionMode();
if (mPendingRequestArgs != null) {
@@ -2814,16 +2798,30 @@
}
return v;
- } else {
- List<ViewGroup> containers = new ArrayList<>(mWorkspace.getPanelCount() + 1);
- containers.add(mWorkspace.getHotseat().getShortcutsAndWidgets());
- mWorkspace.forEachVisiblePage(page
- -> containers.add(((CellLayout) page).getShortcutsAndWidgets()));
-
- // Order: Preferred item by itself or in folder, then by matching package/user
- return getFirstMatch(containers, preferredItem, forFolderMatch(preferredItem),
- packageAndUserAndApp, forFolderMatch(packageAndUserAndApp));
}
+
+ // Look for the item inside the folder at the current page
+ Folder folder = Folder.getOpen(this);
+ if (folder != null) {
+ View v = getFirstMatch(Collections.singletonList(
+ folder.getContent().getCurrentCellLayout().getShortcutsAndWidgets()),
+ preferredItem,
+ packageAndUserAndApp);
+ if (v == null) {
+ folder.close(isStarted() && !isForceInvisible());
+ } else {
+ return v;
+ }
+ }
+
+ List<ViewGroup> containers = new ArrayList<>(mWorkspace.getPanelCount() + 1);
+ containers.add(mWorkspace.getHotseat().getShortcutsAndWidgets());
+ mWorkspace.forEachVisiblePage(page
+ -> containers.add(((CellLayout) page).getShortcutsAndWidgets()));
+
+ // Order: Preferred item by itself or in folder, then by matching package/user
+ return getFirstMatch(containers, preferredItem, forFolderMatch(preferredItem),
+ packageAndUserAndApp, forFolderMatch(packageAndUserAndApp));
}
/**
@@ -2876,7 +2874,16 @@
/**
* Informs us that the overlay (-1 screen, typically), has either become visible or invisible.
*/
- public void onOverlayVisibilityChanged(boolean visible) {}
+ public void onOverlayVisibilityChanged(boolean visible) {
+ getStatsLogManager().logger()
+ .withSrcState(LAUNCHER_STATE_HOME)
+ .withDstState(LAUNCHER_STATE_HOME)
+ .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
+ .setWorkspace(WorkspaceContainer.newBuilder()
+ .setPageIndex(visible ? 0 : -1))
+ .build())
+ .log(visible ? LAUNCHER_SWIPELEFT : LAUNCHER_SWIPERIGHT);
+ }
/**
* Informs us that the page transition has ended, so that we can react to the newly selected
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index 8b342ea..7a74d7e 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -152,7 +152,7 @@
// No need to add padding when cell layout border spacing is present.
boolean noPaddingX =
(dp.cellLayoutBorderSpacePx.x > 0 && mContainerType == WORKSPACE)
- || (dp.folderCellLayoutBorderSpacePx.x > 0 && mContainerType == FOLDER)
+ || (dp.folderCellLayoutBorderSpacePx > 0 && mContainerType == FOLDER)
|| (dp.hotseatBorderSpace > 0 && mContainerType == HOTSEAT);
int cellPaddingX = noPaddingX
? 0
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index f834cce..27e1ba1 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -26,7 +26,6 @@
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.LauncherState.SPRING_LOADED;
import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
-import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_OVERLAY;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
@@ -58,7 +57,6 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import android.widget.Toast;
@@ -119,6 +117,7 @@
import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
import com.android.launcher3.widget.util.WidgetSizes;
import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlay;
+import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks;
import java.util.ArrayList;
import java.util.Iterator;
@@ -136,7 +135,7 @@
public class Workspace<T extends View & PageIndicator> extends PagedView<T>
implements DropTarget, DragSource, View.OnTouchListener,
DragController.DragListener, Insettable, StateHandler<LauncherState>,
- WorkspaceLayoutManager, LauncherBindableItemsContainer {
+ WorkspaceLayoutManager, LauncherBindableItemsContainer, LauncherOverlayCallbacks {
/** The value that {@link #mTransitionProgress} must be greater than for
* {@link #transitionStateShouldAllowDrop()} to return true. */
@@ -254,14 +253,12 @@
// State related to Launcher Overlay
private OverlayEdgeEffect mOverlayEdgeEffect;
- boolean mOverlayShown = false;
- private Runnable mOnOverlayHiddenCallback;
+ private boolean mOverlayShown = false;
+ private float mOverlayProgress; // 1 -> overlay completely visible, 0 -> home visible
+ private final List<LauncherOverlayCallbacks> mOverlayCallbacks = new ArrayList<>();
private boolean mForceDrawAdjacentPages = false;
- // Total over scrollX in the overlay direction.
- private float mOverlayTranslation;
-
// Handles workspace state transitions
private final WorkspaceStateTransitionAnimation mStateTransitionAnimation;
@@ -1151,9 +1148,15 @@
}
public void setLauncherOverlay(LauncherOverlay overlay) {
- mOverlayEdgeEffect = overlay == null ? null : new OverlayEdgeEffect(getContext(), overlay);
- EdgeEffectCompat newEffect = overlay == null
- ? new EdgeEffectCompat(getContext()) : mOverlayEdgeEffect;
+ final EdgeEffectCompat newEffect;
+ if (overlay == null) {
+ newEffect = new EdgeEffectCompat(getContext());
+ mOverlayEdgeEffect = null;
+ } else {
+ newEffect = mOverlayEdgeEffect = new OverlayEdgeEffect(getContext(), overlay);
+ overlay.setOverlayCallbacks(this);
+ }
+
if (mIsRtl) {
mEdgeGlowRight = newEffect;
} else {
@@ -1203,132 +1206,46 @@
@Override
protected boolean shouldFlingForVelocity(int velocityX) {
// When the overlay is moving, the fling or settle transition is controlled by the overlay.
- return Float.compare(Math.abs(mOverlayTranslation), 0) == 0 &&
- super.shouldFlingForVelocity(velocityX);
+ return Float.compare(Math.abs(mOverlayProgress), 0) == 0
+ && super.shouldFlingForVelocity(velocityX);
}
/**
* The overlay scroll is being controlled locally, just update our overlay effect
*/
+ @Override
public void onOverlayScrollChanged(float scroll) {
- if (Float.compare(scroll, 1f) == 0) {
+ mOverlayProgress = Utilities.boundToRange(scroll, 0, 1);
+ if (Float.compare(mOverlayProgress, 1f) == 0) {
if (!mOverlayShown) {
- mLauncher.getStatsLogManager().logger()
- .withSrcState(LAUNCHER_STATE_HOME)
- .withDstState(LAUNCHER_STATE_HOME)
- .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
- .setWorkspace(
- LauncherAtom.WorkspaceContainer.newBuilder()
- .setPageIndex(0))
- .build())
- .log(LAUNCHER_SWIPELEFT);
+ mOverlayShown = true;
+ mLauncher.onOverlayVisibilityChanged(true);
}
- mOverlayShown = true;
-
- // Let the Launcher activity know that the overlay is now visible.
- mLauncher.onOverlayVisibilityChanged(mOverlayShown);
-
- // Not announcing the overlay page for accessibility since it announces itself.
- } else if (Float.compare(scroll, 0f) == 0) {
+ } else if (Float.compare(mOverlayProgress, 0f) == 0) {
if (mOverlayShown) {
- // TODO: this is logged unnecessarily on home gesture.
- mLauncher.getStatsLogManager().logger()
- .withSrcState(LAUNCHER_STATE_HOME)
- .withDstState(LAUNCHER_STATE_HOME)
- .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder()
- .setWorkspace(
- LauncherAtom.WorkspaceContainer.newBuilder()
- .setPageIndex(-1))
- .build())
- .log(LAUNCHER_SWIPERIGHT);
- } else if (Float.compare(mOverlayTranslation, 0f) != 0) {
- // When arriving to 0 overscroll from non-zero overscroll, announce page for
- // accessibility since default announcements were disabled while in overscroll
- // state.
- // Not doing this if mOverlayShown because in that case the accessibility service
- // will announce the launcher window description upon regaining focus after
- // switching from the overlay screen.
- announcePageForAccessibility();
+ mOverlayShown = false;
+ mLauncher.onOverlayVisibilityChanged(false);
}
- mOverlayShown = false;
-
- // Let the Launcher activity know that the overlay is no longer visible.
- mLauncher.onOverlayVisibilityChanged(mOverlayShown);
-
- tryRunOverlayCallback();
}
-
- float offset = 0f;
-
- scroll = Math.max(scroll - offset, 0);
- scroll = Math.min(1, scroll / (1 - offset));
-
- float alpha = 1 - Interpolators.DEACCEL_3.getInterpolation(scroll);
- float transX = mLauncher.getDragLayer().getMeasuredWidth() * scroll;
-
- if (mIsRtl) {
- transX = -transX;
+ int count = mOverlayCallbacks.size();
+ for (int i = 0; i < count; i++) {
+ mOverlayCallbacks.get(i).onOverlayScrollChanged(mOverlayProgress);
}
- mOverlayTranslation = transX;
-
- // TODO(adamcohen): figure out a final effect here. We may need to recommend
- // different effects based on device performance. On at least one relatively high-end
- // device I've tried, translating the launcher causes things to get quite laggy.
- mLauncher.getDragLayer().setTranslationX(transX);
- mLauncher.getDragLayer().getAlphaProperty(ALPHA_INDEX_OVERLAY).setValue(alpha);
}
/**
- * @return false if the callback is still pending
+ * Adds a callback for receiving overlay progress
*/
- private boolean tryRunOverlayCallback() {
- if (mOnOverlayHiddenCallback == null) {
- // Return true as no callback is pending. This is used by OnWindowFocusChangeListener
- // to remove itself if multiple focus handles were added.
- return true;
- }
- if (mOverlayShown || !hasWindowFocus()) {
- return false;
- }
-
- mOnOverlayHiddenCallback.run();
- mOnOverlayHiddenCallback = null;
- return true;
+ public void addOverlayCallback(LauncherOverlayCallbacks callback) {
+ mOverlayCallbacks.add(callback);
+ callback.onOverlayScrollChanged(mOverlayProgress);
}
/**
- * Runs the given callback when the minus one overlay is hidden. Specifically, it is run
- * when launcher's window has focus and the overlay is no longer being shown. If a callback
- * is already present, the new callback will chain off it so both are run.
- *
- * @return Whether the callback was deferred.
+ * Removes a previously added overlay progress callback
*/
- public boolean runOnOverlayHidden(Runnable callback) {
- if (mOnOverlayHiddenCallback == null) {
- mOnOverlayHiddenCallback = callback;
- } else {
- // Chain the new callback onto the previous callback(s).
- Runnable oldCallback = mOnOverlayHiddenCallback;
- mOnOverlayHiddenCallback = () -> {
- oldCallback.run();
- callback.run();
- };
- }
- if (!tryRunOverlayCallback()) {
- ViewTreeObserver observer = getViewTreeObserver();
- if (observer != null && observer.isAlive()) {
- observer.addOnWindowFocusChangeListener(
- new ViewTreeObserver.OnWindowFocusChangeListener() {
- @Override
- public void onWindowFocusChanged(boolean hasFocus) {
- if (tryRunOverlayCallback() && observer.isAlive()) {
- observer.removeOnWindowFocusChangeListener(this);
- }
- }});
- }
- return true;
- }
- return false;
+ public void removeOverlayCallback(LauncherOverlayCallbacks callback) {
+ mOverlayCallbacks.remove(callback);
}
@Override
@@ -2467,20 +2384,20 @@
final View child = (mDragInfo == null) ? null : mDragInfo.cell;
int reorderX = mTargetCell[0];
int reorderY = mTargetCell[1];
+ if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
+ && (mLastReorderX != reorderX || mLastReorderY != reorderY)
+ && targetCellDistance < mDragTargetLayout.getReorderRadius(mTargetCell, item.spanX,
+ item.spanY)) {
+ mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
+ (int) mDragViewVisualCenter[1], minSpanX, minSpanY, item.spanX, item.spanY,
+ child, mTargetCell, new int[2], CellLayout.MODE_SHOW_REORDER_HINT);
+ }
+
if (!nearestDropOccupied) {
mDragTargetLayout.visualizeDropLocation(mTargetCell[0], mTargetCell[1],
item.spanX, item.spanY, d);
} else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
- && !mReorderAlarm.alarmPending()
- && (mLastReorderX != reorderX || mLastReorderY != reorderY)
- && targetCellDistance < mDragTargetLayout.getReorderRadius(mTargetCell, item.spanX,
- item.spanY)) {
-
- int[] resultSpan = new int[2];
- mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
- (int) mDragViewVisualCenter[1], minSpanX, minSpanY, item.spanX, item.spanY,
- child, mTargetCell, resultSpan, CellLayout.MODE_SHOW_REORDER_HINT);
-
+ && !mReorderAlarm.alarmPending()) {
// Otherwise, if we aren't adding to or creating a folder and there's no pending
// reorder, then we schedule a reorder
ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter,
@@ -3467,7 +3384,7 @@
protected boolean canAnnouncePageDescription() {
// Disable announcements while overscrolling potentially to overlay screen because if we end
// up on the overlay screen, it will take care of announcing itself.
- return Float.compare(mOverlayTranslation, 0f) == 0;
+ return Float.compare(mOverlayProgress, 0f) == 0;
}
@Override
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index fa2c6e9..9930abe 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -141,6 +141,9 @@
private final Launcher mLauncher;
private boolean mIsVerticalLayout;
+ // Whether this class should take care of closing the keyboard.
+ private boolean mShouldControlKeyboard;
+
// Animation in this class is controlled by a single variable {@link mProgress}.
// Visually, it represents top y coordinate of the all apps container if multiplied with
// {@link mShiftRange}.
@@ -238,7 +241,7 @@
StateAnimationConfig config, PendingAnimation builder) {
if (mLauncher.isInState(ALL_APPS) && !ALL_APPS.equals(toState)) {
// For atomic animations, we close the keyboard immediately.
- if (!config.userControlled && !mLauncher.getSearchConfig().isKeyboardSyncEnabled()) {
+ if (!config.userControlled && mShouldControlKeyboard) {
mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
}
@@ -252,7 +255,7 @@
// the keyboard open and then changes their mind and swipes back up, we want the
// keyboard to remain open. However an onCancel signal is sent to the listeners
// (success = false), so we need to check for that.
- if (config.userControlled && success) {
+ if (config.userControlled && success && mShouldControlKeyboard) {
mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
}
});
@@ -317,6 +320,8 @@
mAppsViewAlpha.setUpdateVisibility(true);
mAppsViewTranslationY = new MultiPropertyFactory<>(
mAppsView, VIEW_TRANSLATE_Y, APPS_VIEW_INDEX_COUNT, Float::sum);
+
+ mShouldControlKeyboard = !mLauncher.getSearchConfig().isKeyboardSyncEnabled();
}
/**
@@ -333,7 +338,9 @@
private void onProgressAnimationEnd() {
if (Float.compare(mProgress, 1f) == 0) {
mAppsView.reset(false /* animate */);
- mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
+ if (mShouldControlKeyboard) {
+ mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
+ }
}
}
}
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
index 2ff992e..da86d98 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
@@ -107,8 +107,7 @@
new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
- updateHeaderScroll(
- ((AllAppsRecyclerView) recyclerView).computeVerticalScrollOffset());
+ updateHeaderScroll(recyclerView.computeVerticalScrollOffset());
}
};
@@ -192,7 +191,6 @@
reset(true);
}
}
-
}
@Override
@@ -815,6 +813,10 @@
}
}
+ protected void onInitializeRecyclerView(RecyclerView rv) {
+ rv.addOnScrollListener(mScrollListener);
+ }
+
/** Holds a {@link BaseAllAppsAdapter} and related fields. */
public class AdapterHolder {
public static final int MAIN = 0;
@@ -851,7 +853,7 @@
mRecyclerView.setHasFixedSize(true);
// No animations will occur when changes occur to the items in this RecyclerView.
mRecyclerView.setItemAnimator(null);
- mRecyclerView.addOnScrollListener(mScrollListener);
+ onInitializeRecyclerView(mRecyclerView);
FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mRecyclerView);
mRecyclerView.addItemDecoration(focusedItemDecorator);
mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
diff --git a/src/com/android/launcher3/allapps/BaseSearchConfig.java b/src/com/android/launcher3/allapps/BaseSearchConfig.java
index 9f47e8d..3900954 100644
--- a/src/com/android/launcher3/allapps/BaseSearchConfig.java
+++ b/src/com/android/launcher3/allapps/BaseSearchConfig.java
@@ -25,4 +25,11 @@
public boolean isKeyboardSyncEnabled() {
return false;
}
+
+ /**
+ * Returns whether IME is enabled on swipe up.
+ */
+ public boolean isImeEnabledOnSwipeUp() {
+ return false;
+ }
}
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
index 20f5e74..92e29bb 100644
--- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -16,19 +16,50 @@
package com.android.launcher3.allapps;
import android.content.Context;
+import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.WindowInsets;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.ExtendedEditText;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
/**
* AllAppsContainerView with launcher specific callbacks
*/
public class LauncherAllAppsContainerView extends ActivityAllAppsContainerView<Launcher> {
+ private final RecyclerView.OnScrollListener mActivityScrollListener =
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ int scrolledOffset = recyclerView.computeVerticalScrollOffset();
+ ExtendedEditText input = mSearchUiManager.getEditText();
+ // Scroll up and scroll to top
+ if (dy < 0 && scrolledOffset == 0 && input != null) {
+ boolean isImeEnabledOnSwipeUp = Launcher.getLauncher(mActivityContext)
+ .getSearchConfig().isImeEnabledOnSwipeUp();
+ if (isImeEnabledOnSwipeUp || !TextUtils.isEmpty(input.getText())) {
+ input.showKeyboard();
+ }
+ }
+ }
+ };
+
+ @Override
+ protected void onInitializeRecyclerView(RecyclerView rv) {
+ super.onInitializeRecyclerView(rv);
+ if (FeatureFlags.SCROLL_TOP_TO_RESET.get()) {
+ rv.addOnScrollListener(mActivityScrollListener);
+ }
+ }
+
public LauncherAllAppsContainerView(Context context) {
this(context, null);
}
diff --git a/src/com/android/launcher3/anim/PropertySetter.java b/src/com/android/launcher3/anim/PropertySetter.java
index b0ed2d2..6ba58b6 100644
--- a/src/com/android/launcher3/anim/PropertySetter.java
+++ b/src/com/android/launcher3/anim/PropertySetter.java
@@ -38,6 +38,7 @@
public void add(Animator animatorSet) {
animatorSet.setDuration(0);
animatorSet.start();
+ animatorSet.end();
}
};
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 037a77e..4287779 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -96,6 +96,10 @@
"ENABLE_QUICK_LAUNCH_V2", false, "Use quick launch v2 "
+ "behavior. Quick search and quick launch v1 would be unavailable if this is enabled");
+ public static final BooleanFlag GBOARD_UPDATE_ENTER_KEY = new DeviceFlag(
+ "GBOARD_UPDATE_ENTER_KEY", false, "Update gBoard enter key "
+ + "icon dynamically based on top search content for Quick Launch V2");
+
public static final BooleanFlag ENABLE_HIDE_HEADER = new DeviceFlag("ENABLE_HIDE_HEADER",
true, "Hide header on keyboard before typing in all apps");
@@ -206,10 +210,6 @@
public static final BooleanFlag ENABLE_ENFORCED_ROUNDED_CORNERS = new DeviceFlag(
"ENABLE_ENFORCED_ROUNDED_CORNERS", true, "Enforce rounded corners on all App Widgets");
- public static final BooleanFlag ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER = new DeviceFlag(
- "ENABLE_LOCAL_RECOMMENDED_WIDGETS_FILTER", true,
- "Enables a local filter for recommended widgets.");
-
public static final BooleanFlag NOTIFY_CRASHES = getDebugFlag("NOTIFY_CRASHES", false,
"Sends a notification whenever launcher encounters an uncaught exception.");
@@ -253,6 +253,10 @@
"ENABLE_SPLIT_FROM_WORKSPACE", true,
"Enable initiating split screen from workspace.");
+ public static final BooleanFlag ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS =
+ getDebugFlag("ENABLE_SPLIT_FROM_FULLSCREEN_SHORTCUT", false,
+ "Enable splitting from fullscreen app with keyboard shortcuts");
+
public static final BooleanFlag ENABLE_NEW_MIGRATION_LOGIC = getDebugFlag(
"ENABLE_NEW_MIGRATION_LOGIC", true,
"Enable the new grid migration logic, keeping pages when src < dest");
@@ -290,6 +294,10 @@
public static final BooleanFlag ENABLE_WIDGET_PICKER_DEPTH = new DeviceFlag(
"ENABLE_WIDGET_PICKER_DEPTH", true, "Enable changing depth in widget picker.");
+ public static final BooleanFlag SCROLL_TOP_TO_RESET = new DeviceFlag(
+ "SCROLL_TOP_TO_RESET", false, "Bring up IME and focus on "
+ + "input when scroll to top if 'Always show keyboard' is enabled or in prefix state");
+
public static final BooleanFlag SHOW_DELIGHTFUL_PAGINATION = getDebugFlag(
"SHOW_DELIGHTFUL_PAGINATION", false,
"Enable showing the new 'delightful pagination' which is a brand"
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 4bea0ad..1ee7fc1 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -47,7 +47,9 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.ShortcutAndWidgetContainer;
+import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
+import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.anim.SpringProperty;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
@@ -56,13 +58,14 @@
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
+import com.android.systemui.plugins.shared.LauncherOverlayManager.LauncherOverlayCallbacks;
import java.util.ArrayList;
/**
* A ViewGroup that coordinates dragging across its descendants
*/
-public class DragLayer extends BaseDragLayer<Launcher> {
+public class DragLayer extends BaseDragLayer<Launcher> implements LauncherOverlayCallbacks {
public static final int ALPHA_INDEX_OVERLAY = 0;
private static final int ALPHA_CHANNEL_COUNT = 1;
@@ -70,6 +73,8 @@
public static final int ANIMATION_END_DISAPPEAR = 0;
public static final int ANIMATION_END_REMAIN_VISIBLE = 2;
+ private final boolean mIsRtl;
+
private DragController mDragController;
// Variables relating to animation of views after drop
@@ -100,6 +105,7 @@
setChildrenDrawingOrderEnabled(true);
mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
+ mIsRtl = Utilities.isRtl(getResources());
}
/**
@@ -109,6 +115,7 @@
mDragController = dragController;
recreateControllers();
mWorkspaceDragScrim = new Scrim(this);
+ workspace.addOverlayCallback(this);
}
@Override
@@ -476,4 +483,16 @@
controller.onOneHandedModeStateChanged(activated);
}
}
+
+ @Override
+ public void onOverlayScrollChanged(float progress) {
+ float alpha = 1 - Interpolators.DEACCEL_3.getInterpolation(progress);
+ float transX = getMeasuredWidth() * progress;
+
+ if (mIsRtl) {
+ transX = -transX;
+ }
+ setTranslationX(transX);
+ getAlphaProperty(ALPHA_INDEX_OVERLAY).setValue(alpha);
+ }
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 7e14912..99822da 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -292,7 +292,7 @@
mFolderName.forceDisableSuggestions(true);
mFooter = findViewById(R.id.folder_footer);
- mFooterHeight = getResources().getDimensionPixelSize(R.dimen.folder_label_height);
+ mFooterHeight = dp.folderFooterHeightPx;
if (Utilities.ATLEAST_R) {
mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
@@ -1167,14 +1167,6 @@
mContent.setFixedSize(contentWidth, contentHeight);
mContent.measure(contentAreaWidthSpec, contentAreaHeightSpec);
- if (mContent.getChildCount() > 0) {
- int cellIconGap = (mContent.getPageAt(0).getCellWidth()
- - mActivityContext.getDeviceProfile().iconSizePx) / 2;
- mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap,
- mFooter.getPaddingTop(),
- mContent.getPaddingRight() + cellIconGap,
- mFooter.getPaddingBottom());
- }
mFooter.measure(contentAreaWidthSpec,
MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY));
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index d5ef9df..4829c42 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -234,9 +234,9 @@
mFolder, startRect, endRect, finalRadius, !mIsOpening));
// Create reveal animator for the folder content (capture the top 4 icons 2x2)
- int width = mDeviceProfile.folderCellLayoutBorderSpacePx.x
+ int width = mDeviceProfile.folderCellLayoutBorderSpacePx
+ mDeviceProfile.folderCellWidthPx * 2;
- int height = mDeviceProfile.folderCellLayoutBorderSpacePx.y
+ int height = mDeviceProfile.folderCellLayoutBorderSpacePx
+ mDeviceProfile.folderCellHeightPx * 2;
int page = mIsOpening ? mContent.getCurrentPage() : mContent.getDestinationPage();
int left = mContent.getPaddingLeft() + page * lp.width;
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 05f53fd..fcc5d86 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -617,6 +617,11 @@
@UiEvent(doc = "Number of apps in A-Z list (personal and work profile)")
LAUNCHER_ALLAPPS_COUNT(1225),
+ @UiEvent(doc = "User has invoked split to right half with a keyboard shortcut.")
+ LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_RIGHT_BOTTOM(1232),
+
+ @UiEvent(doc = "User has invoked split to left half with a keyboard shortcut.")
+ LAUNCHER_KEYBOARD_SHORTCUT_SPLIT_LEFT_TOP(1233)
;
// ADD MORE
@@ -781,7 +786,6 @@
public int getId() {
return mId;
}
-
}
/**
@@ -814,6 +818,13 @@
}
/**
+ * Sets sub event type.
+ */
+ default StatsLatencyLogger withSubEventType(int type) {
+ return this;
+ }
+
+ /**
* Sets packageId of log message.
*/
default StatsLatencyLogger withPackageId(int packageId) {
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index 3770de8..e9b6606 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -17,6 +17,7 @@
package com.android.launcher3.pageindicators;
import static com.android.launcher3.config.FeatureFlags.SHOW_DELIGHTFUL_PAGINATION;
+import static com.android.launcher3.config.FeatureFlags.SHOW_DOT_PAGINATION;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -182,7 +183,9 @@
@Override
public void setScroll(int currentScroll, int totalScroll) {
- animatePaginationToAlpha(VISIBLE_ALPHA);
+ if (SHOW_DELIGHTFUL_PAGINATION.get() || SHOW_DOT_PAGINATION.get()) {
+ animatePaginationToAlpha(VISIBLE_ALPHA);
+ }
if (mNumPages <= 1) {
mCurrentScroll = 0;
@@ -193,9 +196,9 @@
currentScroll = totalScroll - currentScroll;
}
+ mTotalScroll = totalScroll;
if (SHOW_DELIGHTFUL_PAGINATION.get()) {
mCurrentScroll = currentScroll;
- mTotalScroll = totalScroll;
invalidate();
if (mShouldAutoHide
@@ -214,9 +217,15 @@
if (currentScroll < pageToLeftScroll + scrollThreshold) {
// scroll is within the left page's threshold
animateToPosition(pageToLeft);
+ if (SHOW_DOT_PAGINATION.get()) {
+ hideAfterDelay();
+ }
} else if (currentScroll > pageToRightScroll - scrollThreshold) {
// scroll is far enough from left page to go to the right page
animateToPosition(pageToLeft + 1);
+ if (SHOW_DOT_PAGINATION.get()) {
+ hideAfterDelay();
+ }
} else {
// scroll is between left and right page
animateToPosition(pageToLeft + SHIFT_PER_ANIMATION);
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 49d27b7..70956a3 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -18,6 +18,7 @@
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
+import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
import android.content.Intent;
@@ -207,7 +208,11 @@
PreferenceScreen screen = getPreferenceScreen();
for (int i = screen.getPreferenceCount() - 1; i >= 0; i--) {
Preference preference = screen.getPreference(i);
- if (!initPreference(preference)) {
+ if (initPreference(preference)) {
+ if (IS_STUDIO_BUILD && preference == mDeveloperOptionPref) {
+ preference.setOrder(0);
+ }
+ } else {
screen.removePreference(preference);
}
}
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 2a890c3..520f33c 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -231,4 +231,10 @@
* etc.)
*/
protected abstract void onHandleConfigurationChanged();
+
+ /**
+ * Enter staged split directly from the current running app.
+ * @param leftOrTop if the staged split will be positioned left or top.
+ */
+ public void enterStageSplitFromRunningApp(boolean leftOrTop) { }
}
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 269baf0..acb7eb3 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -47,7 +47,9 @@
import com.android.launcher3.util.ResourceBasedOverride;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
+import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -214,11 +216,15 @@
}
case TestProtocol.REQUEST_HAS_TIS: {
- response.putBoolean(
- TestProtocol.REQUEST_HAS_TIS, false);
+ response.putBoolean(TestProtocol.REQUEST_HAS_TIS, false);
return response;
}
+ case TestProtocol.REQUEST_ALL_APPS_TOP_PADDING: {
+ return getLauncherUIProperty(Bundle::putInt,
+ l -> l.getAppsView().getActiveRecyclerView().getClipBounds().top);
+ }
+
default:
return null;
}
@@ -261,17 +267,24 @@
*/
private static <S, T> Bundle getUIProperty(
BundleSetter<T> bundleSetter, Function<S, T> provider, Supplier<S> targetSupplier) {
+ return getFromExecutorSync(MAIN_EXECUTOR, () -> {
+ S target = targetSupplier.get();
+ if (target == null) {
+ return null;
+ }
+ T value = provider.apply(target);
+ Bundle response = new Bundle();
+ bundleSetter.set(response, TestProtocol.TEST_INFO_RESPONSE_FIELD, value);
+ return response;
+ });
+ }
+
+ /**
+ * Executes the callback on the executor and waits for the result
+ */
+ protected static <T> T getFromExecutorSync(ExecutorService executor, Callable<T> callback) {
try {
- return MAIN_EXECUTOR.submit(() -> {
- S target = targetSupplier.get();
- if (target == null) {
- return null;
- }
- T value = provider.apply(target);
- Bundle response = new Bundle();
- bundleSetter.set(response, TestProtocol.TEST_INFO_RESPONSE_FIELD, value);
- return response;
- }).get();
+ return executor.submit(callback).get();
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
diff --git a/src/com/android/launcher3/testing/shared/TestProtocol.java b/src/com/android/launcher3/testing/shared/TestProtocol.java
index 91b7b2d..3fbce88 100644
--- a/src/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/src/com/android/launcher3/testing/shared/TestProtocol.java
@@ -112,6 +112,9 @@
"get-activities-created-count";
public static final String REQUEST_GET_ACTIVITIES = "get-activities";
public static final String REQUEST_HAS_TIS = "has-touch-interaction-service";
+ public static final String REQUEST_TASKBAR_ALL_APPS_TOP_PADDING =
+ "taskbar-all-apps-top-padding";
+ public static final String REQUEST_ALL_APPS_TOP_PADDING = "all-apps-top-padding";
public static final String REQUEST_WORKSPACE_CELL_LAYOUT_SIZE = "workspace-cell-layout-size";
public static final String REQUEST_WORKSPACE_CELL_CENTER = "workspace-cell-center";
@@ -125,6 +128,7 @@
public static final String REQUEST_GET_OVERVIEW_PAGE_SPACING = "get-overview-page-spacing";
public static final String REQUEST_ENABLE_ROTATION = "enable_rotation";
public static final String REQUEST_ENABLE_SUGGESTION = "enable-suggestion";
+ public static final String REQUEST_MODEL_QUEUE_CLEARED = "model-queue-cleared";
public static boolean sDebugTracing = false;
public static final String REQUEST_ENABLE_DEBUG_TRACING = "enable-debug-tracing";
diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java
index 3eff783..19a3948 100644
--- a/src/com/android/launcher3/util/SplitConfigurationOptions.java
+++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java
@@ -182,4 +182,12 @@
? LAUNCHER_APP_ICON_MENU_SPLIT_LEFT_TOP
: LAUNCHER_APP_ICON_MENU_SPLIT_RIGHT_BOTTOM;
}
+
+ public static @StagePosition int getOppositeStagePosition(@StagePosition int position) {
+ if (position == STAGE_POSITION_UNDEFINED) {
+ return position;
+ }
+ return position == STAGE_POSITION_TOP_OR_LEFT ? STAGE_POSITION_BOTTOM_OR_RIGHT
+ : STAGE_POSITION_TOP_OR_LEFT;
+ }
}
diff --git a/src_plugins/com/android/systemui/plugins/shared/LauncherExterns.java b/src_plugins/com/android/systemui/plugins/shared/LauncherExterns.java
index 13e4999..173b454 100644
--- a/src_plugins/com/android/systemui/plugins/shared/LauncherExterns.java
+++ b/src_plugins/com/android/systemui/plugins/shared/LauncherExterns.java
@@ -40,10 +40,4 @@
* Sets the overlay on the target activity
*/
void setLauncherOverlay(LauncherOverlay overlay);
-
- /**
- * Executes the command, next time the overlay is hidden
- */
- void runOnOverlayHidden(Runnable runnable);
-
}
diff --git a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
index ac02ba4..582ab23 100644
--- a/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
+++ b/src_plugins/com/android/systemui/plugins/shared/LauncherOverlayManager.java
@@ -93,6 +93,6 @@
interface LauncherOverlayCallbacks {
- void onScrollChanged(float progress);
+ void onOverlayScrollChanged(float progress);
}
}
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index ae1060e..bedf277 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -24,6 +24,7 @@
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application android:debuggable="true" android:extractNativeLibs="true">
<uses-library android:name="android.test.runner"/>
@@ -62,6 +63,17 @@
</receiver>
<receiver
+ android:name="com.android.launcher3.testcomponent.AppWidgetWithDialog"
+ android:exported="true"
+ android:label="With Dialog">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider"
+ android:resource="@xml/appwidget_no_config"/>
+ </receiver>
+
+ <receiver
android:name="com.android.launcher3.testcomponent.AppWidgetDynamicColors"
android:exported="true"
android:label="Dynamic Colors">
@@ -276,7 +288,17 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
-
+ <activity
+ android:name="com.android.launcher3.testcomponent.DialogTestActivity"
+ android:label="Dialog Activity"
+ android:theme="@android:style/Theme.Dialog"
+ android:exported="true"
+ android:taskAffinity="com.android.launcher3.testcomponent.Affinity2">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
<activity android:name="com.android.launcher3.testcomponent.ImeTestActivity"
android:label="ImeTestActivity"
android:icon="@drawable/test_theme_icon"
diff --git a/tests/res/layout/test_layout_appwidget_blue.xml b/tests/res/layout/test_layout_appwidget_blue.xml
index 8111978..f33e575 100644
--- a/tests/res/layout/test_layout_appwidget_blue.xml
+++ b/tests/res/layout/test_layout_appwidget_blue.xml
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/content"
android:orientation="vertical"
android:background="#FF0000FF"
android:layout_width="match_parent"
diff --git a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt b/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt
index 2c1cbdf..fe30fdc 100644
--- a/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt
+++ b/tests/src/com/android/launcher3/DeviceProfileBaseTest.kt
@@ -16,11 +16,12 @@
package com.android.launcher3
import android.content.Context
+import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
import android.util.SparseArray
import androidx.test.core.app.ApplicationProvider
-import com.android.launcher3.DeviceProfile.DEFAULT_PROVIDER;
+import com.android.launcher3.DeviceProfile.DEFAULT_PROVIDER
import com.android.launcher3.util.DisplayController.Info
import com.android.launcher3.util.WindowBounds
import org.junit.Before
@@ -116,10 +117,7 @@
numFolderRows = 3
numFolderColumns = 3
- folderBorderSpaces = PointF(16f, 16f)
- folderTopPadding = 24f
- folderCellSize = PointF(80f, 94f)
-
+ folderStyle = R.style.FolderDefaultStyle
inlineNavButtonsEndSpacing = R.dimen.taskbar_button_margin_4_5
@@ -207,9 +205,7 @@
numFolderRows = 3
numFolderColumns = 3
- folderBorderSpaces = PointF(16f, 16f)
- folderTopPadding = 24f
- folderCellSize = PointF(120f, 104f)
+ folderStyle = R.style.FolderDefaultStyle
inlineNavButtonsEndSpacing = R.dimen.taskbar_button_margin_6_5
@@ -241,7 +237,7 @@
numAllAppsColumns = 6
isScalable = true
- devicePaddingId = 2132148242 // "@xml/paddings_6x5"
+ devicePaddingId = R.xml.paddings_6x5
inlineQsb = booleanArrayOf(
false,
@@ -305,9 +301,7 @@
numFolderRows = 3
numFolderColumns = 3
- folderBorderSpaces = PointF(16f, 16f)
- folderTopPadding = 24f
- folderCellSize = PointF(80f, 94f)
+ folderStyle = R.style.FolderDefaultStyle
inlineNavButtonsEndSpacing = R.dimen.taskbar_button_margin_4_4
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
index 2846cae..9ba3f7b 100644
--- a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
+++ b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
@@ -116,7 +116,7 @@
FavoriteItemsTransaction transaction =
new FavoriteItemsTransaction(mTargetContext, this);
mWorkspaceBuilder.buildFromBoard(testCase.mStart, transaction).commit();
-
+ waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
Widget widget = mLauncher.getWorkspace().getWidgetAtCell(mainWidgetCellPos.x,
mainWidgetCellPos.y);
assertNotNull(widget);
diff --git a/tests/src/com/android/launcher3/testcomponent/AppWidgetWithDialog.java b/tests/src/com/android/launcher3/testcomponent/AppWidgetWithDialog.java
new file mode 100644
index 0000000..6d617fa
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/AppWidgetWithDialog.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 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.testcomponent;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.Intent;
+import android.widget.RemoteViews;
+
+/**
+ * A simple app widget with shows a dialog on clicking.
+ */
+public class AppWidgetWithDialog extends AppWidgetNoConfig {
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ int layoutId = context.getResources().getIdentifier(
+ "test_layout_appwidget_blue", "layout", context.getPackageName());
+ RemoteViews views = new RemoteViews(context.getPackageName(), layoutId);
+
+ PendingIntent pi = PendingIntent.getActivity(context, 0,
+ new Intent(context, DialogTestActivity.class), PendingIntent.FLAG_IMMUTABLE);
+ views.setOnClickPendingIntent(android.R.id.content, pi);
+ AppWidgetManager.getInstance(context).updateAppWidget(appWidgetIds, views);
+ }
+}
diff --git a/tests/src/com/android/launcher3/testcomponent/DialogTestActivity.java b/tests/src/com/android/launcher3/testcomponent/DialogTestActivity.java
new file mode 100644
index 0000000..9e5a274
--- /dev/null
+++ b/tests/src/com/android/launcher3/testcomponent/DialogTestActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 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.testcomponent;
+
+
+/**
+ * Extension of BaseTestingActivity with a Dialog theme
+ */
+public class DialogTestActivity extends BaseTestingActivity {}
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index cf5f5fc..50e0990 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -335,6 +335,7 @@
@IwTest(focusArea="launcher")
@Test
@PortraitLandscape
+ @ScreenRecord // b/256898879
public void testDragAppIcon() throws Throwable {
// 1. Open all apps and wait for load complete.
// 2. Drag icon to homescreen.
@@ -478,7 +479,7 @@
@Test
@PortraitLandscape
public void testUninstallFromWorkspace() throws Exception {
- TestUtil.installDummyApp();
+ installDummyAppAndWaitForUIUpdate();
try {
verifyAppUninstalledFromAllApps(
createShortcutInCenterIfNotExist(DUMMY_APP_NAME).uninstall(), DUMMY_APP_NAME);
@@ -489,8 +490,9 @@
@Test
@PortraitLandscape
+ @ScreenRecord // (b/256659409)
public void testUninstallFromAllApps() throws Exception {
- TestUtil.installDummyApp();
+ installDummyAppAndWaitForUIUpdate();
try {
Workspace workspace = mLauncher.getWorkspace();
final HomeAllApps allApps = workspace.switchToAllApps();
@@ -535,7 +537,7 @@
Point[] gridPositions = getCornersAndCenterPositions();
createShortcutIfNotExist(STORE_APP_NAME, gridPositions[0]);
createShortcutIfNotExist(MAPS_APP_NAME, gridPositions[1]);
- TestUtil.installDummyApp();
+ installDummyAppAndWaitForUIUpdate();
try {
createShortcutIfNotExist(DUMMY_APP_NAME, gridPositions[2]);
Map<String, Point> initialPositions =
@@ -586,6 +588,17 @@
mLauncher.getWorkspace().getHotseatAppIcon(APP_NAME));
}
+ private void installDummyAppAndWaitForUIUpdate() throws IOException {
+ TestUtil.installDummyApp();
+ // Wait for model thread completion as it may be processing
+ // the install event from the SystemService
+ mLauncher.waitForModelQueueCleared();
+ // Wait for Launcher UI thread completion, as it may be processing updating the UI in
+ // response to the model update. Not that `waitForLauncherInitialized` is just a proxy
+ // method, we can use any method which touches Launcher UI thread,
+ mLauncher.waitForLauncherInitialized();
+ }
+
/**
* @return List of workspace grid coordinates. Those are not pixels. See {@link
* Workspace#getIconGridDimensions()}
diff --git a/tests/src/com/android/launcher3/util/TestUtil.java b/tests/src/com/android/launcher3/util/TestUtil.java
index 67f3902..d7c6c4f 100644
--- a/tests/src/com/android/launcher3/util/TestUtil.java
+++ b/tests/src/com/android/launcher3/util/TestUtil.java
@@ -17,8 +17,13 @@
import static androidx.test.InstrumentationRegistry.getContext;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static androidx.test.InstrumentationRegistry.getTargetContext;
+import android.content.pm.LauncherApps;
import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
import androidx.test.uiautomator.UiDevice;
@@ -27,6 +32,7 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.util.concurrent.CountDownLatch;
public class TestUtil {
public static final String DUMMY_PACKAGE = "com.example.android.aardwolf";
@@ -40,24 +46,77 @@
final String apkFilename = getInstrumentation().getTargetContext().
getFilesDir().getPath() + "/dummy_app.apk";
- final FileOutputStream out = new FileOutputStream(apkFilename);
- byte[] buff = new byte[1024];
- int read;
+ try (PackageInstallCheck pic = new PackageInstallCheck()) {
+ final FileOutputStream out = new FileOutputStream(apkFilename);
+ byte[] buff = new byte[1024];
+ int read;
- while ((read = in.read(buff)) > 0) {
- out.write(buff, 0, read);
+ while ((read = in.read(buff)) > 0) {
+ out.write(buff, 0, read);
+ }
+ in.close();
+ out.close();
+
+ final String result = UiDevice.getInstance(getInstrumentation())
+ .executeShellCommand("pm install " + apkFilename);
+ Assert.assertTrue(
+ "Failed to install wellbeing test apk; make sure the device is rooted",
+ "Success".equals(result.replaceAll("\\s+", "")));
+ pic.mAddWait.await();
+ } catch (InterruptedException e) {
+ throw new IOException(e);
}
- in.close();
- out.close();
-
- final String result = UiDevice.getInstance(getInstrumentation())
- .executeShellCommand("pm install " + apkFilename);
- Assert.assertTrue("Failed to install wellbeing test apk; make sure the device is rooted",
- "Success".equals(result.replaceAll("\\s+", "")));
}
public static void uninstallDummyApp() throws IOException {
UiDevice.getInstance(getInstrumentation()).executeShellCommand(
"pm uninstall " + DUMMY_PACKAGE);
}
+
+ private static class PackageInstallCheck extends LauncherApps.Callback
+ implements AutoCloseable {
+
+ final CountDownLatch mAddWait = new CountDownLatch(1);
+ final LauncherApps mLauncherApps;
+
+ PackageInstallCheck() {
+ mLauncherApps = getTargetContext().getSystemService(LauncherApps.class);
+ mLauncherApps.registerCallback(this, new Handler(Looper.getMainLooper()));
+ }
+
+ private void verifyPackage(String packageName) {
+ if (DUMMY_PACKAGE.equals(packageName)) {
+ mAddWait.countDown();
+ }
+ }
+
+ @Override
+ public void onPackageAdded(String packageName, UserHandle user) {
+ verifyPackage(packageName);
+ }
+
+ @Override
+ public void onPackageChanged(String packageName, UserHandle user) {
+ verifyPackage(packageName);
+ }
+
+ @Override
+ public void onPackageRemoved(String packageName, UserHandle user) { }
+
+ @Override
+ public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) {
+ for (String packageName : packageNames) {
+ verifyPackage(packageName);
+ }
+ }
+
+ @Override
+ public void onPackagesUnavailable(String[] packageNames, UserHandle user,
+ boolean replacing) { }
+
+ @Override
+ public void close() {
+ mLauncherApps.unregisterCallback(this);
+ }
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 4791846..b0cf20f 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -16,6 +16,9 @@
package com.android.launcher3.tapl;
+import static com.android.launcher3.tapl.LauncherInstrumentation.DEFAULT_POLL_INTERVAL;
+import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS;
+
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
@@ -37,6 +40,9 @@
* Operations on AllApps opened from Home. Also a parent for All Apps opened from Overview.
*/
public abstract class AllApps extends LauncherInstrumentation.VisibleContainer {
+ // Defer updates flag used to defer all apps updates by a test's request.
+ private static final int DEFER_UPDATES_TEST = 1 << 1;
+
private static final int MAX_SCROLL_ATTEMPTS = 40;
private final int mHeight;
@@ -46,8 +52,7 @@
super(launcher);
final UiObject2 allAppsContainer = verifyActiveContainer();
mHeight = mLauncher.getVisibleBounds(allAppsContainer).height();
- final UiObject2 appListRecycler = mLauncher.waitForObjectInContainer(allAppsContainer,
- "apps_list_view");
+ final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer);
// Wait for the recycler to populate.
mLauncher.waitForObjectInContainer(appListRecycler, By.clazz(TextView.class));
verifyNotFrozen("All apps freeze flags upon opening all apps");
@@ -78,6 +83,11 @@
LauncherInstrumentation.log("hasClickableIcon: icon center is under search box");
return false;
}
+ if (iconCenterInRecyclerTopPadding(appListRecycler, icon)) {
+ LauncherInstrumentation.log(
+ "hasClickableIcon: icon center is under the app list recycler's top padding.");
+ return false;
+ }
if (iconBounds.bottom > displayBottom) {
LauncherInstrumentation.log("hasClickableIcon: icon bottom below bottom offset");
return false;
@@ -92,6 +102,13 @@
iconCenter.x, iconCenter.y);
}
+ private boolean iconCenterInRecyclerTopPadding(UiObject2 appListRecycler, UiObject2 icon) {
+ final Point iconCenter = icon.getVisibleCenter();
+
+ return iconCenter.y <= mLauncher.getVisibleBounds(appListRecycler).top
+ + getAppsListRecyclerTopPadding();
+ }
+
/**
* Finds an icon. If the icon doesn't exist, return null.
* Scrolls the app list when needed to make sure the icon is visible.
@@ -105,9 +122,7 @@
LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"getting app icon " + appName + " on all apps")) {
final UiObject2 allAppsContainer = verifyActiveContainer();
- final UiObject2 appListRecycler = mLauncher.waitForObjectInContainer(allAppsContainer,
- "apps_list_view");
- final UiObject2 searchBox = hasSearchBox() ? getSearchBox(allAppsContainer) : null;
+ final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer);
int deviceHeight = mLauncher.getRealDisplaySize().y;
int bottomGestureStartOnScreen = mLauncher.getBottomGestureStartOnScreen();
@@ -128,10 +143,9 @@
mLauncher.getVisibleBounds(icon).top
< bottomGestureStartOnScreen)
.collect(Collectors.toList()),
- hasSearchBox()
- ? mLauncher.getVisibleBounds(searchBox).bottom
- - mLauncher.getVisibleBounds(allAppsContainer).top
- : 0);
+ mLauncher.getVisibleBounds(appListRecycler).top
+ + getAppsListRecyclerTopPadding()
+ - mLauncher.getVisibleBounds(allAppsContainer).top);
verifyActiveContainer();
final int newScroll = getAllAppsScroll();
mLauncher.assertTrue(
@@ -180,16 +194,22 @@
protected abstract boolean hasSearchBox();
+ protected abstract int getAppsListRecyclerTopPadding();
+
private void scrollBackToBeginning() {
try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"want to scroll back in all apps")) {
LauncherInstrumentation.log("Scrolling to the beginning");
final UiObject2 allAppsContainer = verifyActiveContainer();
- final UiObject2 searchBox = hasSearchBox() ? getSearchBox(allAppsContainer) : null;
+ final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer);
int attempts = 0;
final Rect margins = new Rect(
- 0, hasSearchBox() ? mLauncher.getVisibleBounds(searchBox).bottom + 1 : 0, 0, 5);
+ /* left= */ 0,
+ mLauncher.getVisibleBounds(appListRecycler).top
+ + getAppsListRecyclerTopPadding() + 1,
+ /* right= */ 0,
+ /* bottom= */ 5);
for (int scroll = getAllAppsScroll();
scroll != 0;
@@ -220,6 +240,10 @@
.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
+ private UiObject2 getAppListRecycler(UiObject2 allAppsContainer) {
+ return mLauncher.waitForObjectInContainer(allAppsContainer, "apps_list_view");
+ }
+
private UiObject2 getSearchBox(UiObject2 allAppsContainer) {
return mLauncher.waitForObjectInContainer(allAppsContainer, "search_container_all_apps");
}
@@ -274,12 +298,16 @@
*/
public void unfreeze() {
mLauncher.getTestInfo(TestProtocol.REQUEST_UNFREEZE_APP_LIST);
- verifyNotFrozen("All apps freeze flags upon unfreezing");
}
private void verifyNotFrozen(String message) {
+ mLauncher.assertEquals(message, 0, getFreezeFlags() & DEFER_UPDATES_TEST);
+ mLauncher.assertTrue(message, mLauncher.waitAndGet(() -> getFreezeFlags() == 0,
+ WAIT_TIME_MS, DEFAULT_POLL_INTERVAL));
+ }
+
+ private int getFreezeFlags() {
final Bundle testInfo = mLauncher.getTestInfo(TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS);
- if (testInfo == null) return;
- mLauncher.assertEquals(message, 0, testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD));
+ return testInfo == null ? 0 : testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java
index 5164025..f804e28 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java
@@ -18,6 +18,8 @@
import androidx.annotation.NonNull;
import androidx.test.uiautomator.UiObject2;
+import com.android.launcher3.testing.shared.TestProtocol;
+
/**
* Operations on AllApps opened from the Taskbar.
*/
@@ -48,4 +50,10 @@
protected boolean hasSearchBox() {
return false;
}
+
+ @Override
+ protected int getAppsListRecyclerTopPadding() {
+ return mLauncher.getTestInfo(TestProtocol.REQUEST_TASKBAR_ALL_APPS_TOP_PADDING)
+ .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 15705e7..5a96d95 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -58,7 +58,7 @@
"want to switch from background to overview")) {
verifyActiveContainer();
goToOverviewUnchecked();
- return mLauncher.isFallbackOverview()
+ return mLauncher.is3PLauncher()
? new BaseOverview(mLauncher) : new Overview(mLauncher);
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index f47f710..afeb8d7 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -244,43 +244,39 @@
* Returns if clear all button is visible.
*/
public boolean isClearAllVisible() {
- return mLauncher.hasLauncherObject(mLauncher.getOverviewObjectSelector("clear_all"));
+ return verifyActiveContainer().hasObject(
+ mLauncher.getOverviewObjectSelector("clear_all"));
}
protected boolean isActionsViewVisible() {
+ if (!hasTasks() || isClearAllVisible()) {
+ return false;
+ }
OverviewTask task = mLauncher.isTablet() ? getFocusedTaskForTablet() : getCurrentTask();
if (task == null) {
return false;
}
+ // In tablets, if focused task is not in center, overview actions aren't visible.
+ if (mLauncher.isTablet()
+ && Math.abs(task.getExactCenterX() - mLauncher.getExactScreenCenterX()) >= 1) {
+ return false;
+ }
+ // Overview actions aren't visible for split screen tasks.
return !task.isTaskSplit();
}
private void verifyActionsViewVisibility() {
- if (!hasTasks() || !isActionsViewVisible()) {
- return;
- }
try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"want to assert overview actions view visibility")) {
- if (mLauncher.isTablet() && !isOverviewSnappedToFocusedTaskForTablet()) {
- mLauncher.waitUntilOverviewObjectGone("action_buttons");
- } else {
+ if (isActionsViewVisible()) {
mLauncher.waitForOverviewObject("action_buttons");
+ } else {
+ mLauncher.waitUntilOverviewObjectGone("action_buttons");
}
}
}
/**
- * Returns if focused task is currently snapped task in tablet grid overview.
- */
- private boolean isOverviewSnappedToFocusedTaskForTablet() {
- OverviewTask focusedTask = getFocusedTaskForTablet();
- if (focusedTask == null) {
- return false;
- }
- return Math.abs(focusedTask.getExactCenterX() - mLauncher.getExactScreenCenterX()) < 1;
- }
-
- /**
* Returns Overview focused task if it exists.
*
* @throws IllegalStateException if not run on a tablet device.
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
index 7123de4..9a4c6d4 100644
--- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
@@ -18,6 +18,8 @@
import androidx.annotation.NonNull;
import androidx.test.uiautomator.UiObject2;
+import com.android.launcher3.testing.shared.TestProtocol;
+
public class HomeAllApps extends AllApps {
private static final String BOTTOM_SHEET_RES_ID = "bottom_sheet_background";
@@ -47,6 +49,12 @@
return true;
}
+ @Override
+ protected int getAppsListRecyclerTopPadding() {
+ return mLauncher.getTestInfo(TestProtocol.REQUEST_ALL_APPS_TOP_PADDING)
+ .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+ }
+
/**
* Taps outside bottom sheet to dismiss and return to workspace. Available on tablets only.
* @param tapRight Tap on the right of bottom sheet if true, or left otherwise.
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 4b02ecc..f23a38c 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -55,7 +55,7 @@
public Taskbar getTaskbar() {
try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"want to get the taskbar")) {
- mLauncher.waitForLauncherObject(TASKBAR_RES_ID);
+ mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
return new Taskbar(mLauncher);
}
@@ -67,7 +67,7 @@
public void assertTaskbarHidden() {
try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"waiting for taskbar to be hidden")) {
- mLauncher.waitUntilLauncherObjectGone(TASKBAR_RES_ID);
+ mLauncher.waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
}
}
@@ -77,7 +77,7 @@
public void assertTaskbarVisible() {
try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"waiting for taskbar to be visible")) {
- mLauncher.waitForLauncherObject(TASKBAR_RES_ID);
+ mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
}
}
@@ -92,7 +92,7 @@
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
"want to show the taskbar")) {
- mLauncher.waitUntilLauncherObjectGone(TASKBAR_RES_ID);
+ mLauncher.waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
final long downTime = SystemClock.uptimeMillis();
final int unstashTargetY = mLauncher.getRealDisplaySize().y
@@ -106,7 +106,7 @@
LauncherInstrumentation.log("showTaskbar: sent down");
try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("pressed down")) {
- mLauncher.waitForLauncherObject(TASKBAR_RES_ID);
+ mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, unstashTarget,
LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER);
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 449b7b7..1212c3d 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -83,6 +83,7 @@
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeoutException;
+import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -181,6 +182,7 @@
static final String TASKBAR_RES_ID = "taskbar_view";
private static final String SPLIT_PLACEHOLDER_RES_ID = "split_placeholder";
public static final int WAIT_TIME_MS = 30000;
+ static final long DEFAULT_POLL_INTERVAL = 1000;
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
private static final String ANDROID_PACKAGE = "android";
@@ -559,7 +561,7 @@
private String getVisibleStateMessage() {
if (hasLauncherObject(CONTEXT_MENU_RES_ID)) return "Context Menu";
if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
- if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview";
+ if (hasSystemLauncherObject(OVERVIEW_RES_ID)) return "Overview";
if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
return "LaunchedApp (" + getVisiblePackages() + ")";
@@ -771,73 +773,89 @@
switch (containerType) {
case WORKSPACE: {
waitUntilLauncherObjectGone(APPS_RES_ID);
- waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
- waitUntilLauncherObjectGone(TASKBAR_RES_ID);
- waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+ waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
+ waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+
+ if (is3PLauncher() && isTablet()) {
+ waitForSystemLauncherObject(TASKBAR_RES_ID);
+ } else {
+ waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+ }
return waitForLauncherObject(WORKSPACE_RES_ID);
}
case WIDGETS: {
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
waitUntilLauncherObjectGone(APPS_RES_ID);
- waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
- waitUntilLauncherObjectGone(TASKBAR_RES_ID);
- waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+ waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
+ waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+
+ if (is3PLauncher() && isTablet()) {
+ waitForSystemLauncherObject(TASKBAR_RES_ID);
+ } else {
+ waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+ }
return waitForLauncherObject(WIDGETS_RES_ID);
}
- case TASKBAR_ALL_APPS:
- case HOME_ALL_APPS: {
+ case TASKBAR_ALL_APPS: {
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
- waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
- waitUntilLauncherObjectGone(TASKBAR_RES_ID);
- waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+ waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
+ waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+ waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
return waitForLauncherObject(APPS_RES_ID);
}
- case OVERVIEW: {
+ case HOME_ALL_APPS: {
+ waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
+ waitUntilLauncherObjectGone(WIDGETS_RES_ID);
+ waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
+ waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+
+ if (is3PLauncher() && isTablet()) {
+ waitForSystemLauncherObject(TASKBAR_RES_ID);
+ } else {
+ waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+ }
+
+ return waitForLauncherObject(APPS_RES_ID);
+ }
+ case OVERVIEW:
+ case FALLBACK_OVERVIEW: {
waitUntilLauncherObjectGone(APPS_RES_ID);
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
- waitUntilLauncherObjectGone(TASKBAR_RES_ID);
- waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+ waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
+ waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
- return waitForLauncherObject(OVERVIEW_RES_ID);
+ return waitForSystemLauncherObject(OVERVIEW_RES_ID);
}
case SPLIT_SCREEN_SELECT: {
waitUntilLauncherObjectGone(APPS_RES_ID);
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
- waitUntilLauncherObjectGone(TASKBAR_RES_ID);
+ waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
- waitForLauncherObject(SPLIT_PLACEHOLDER_RES_ID);
- return waitForLauncherObject(OVERVIEW_RES_ID);
- }
- case FALLBACK_OVERVIEW: {
- waitUntilLauncherObjectGone(APPS_RES_ID);
- waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
- waitUntilLauncherObjectGone(WIDGETS_RES_ID);
- waitUntilLauncherObjectGone(TASKBAR_RES_ID);
- waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
-
- return waitForFallbackLauncherObject(OVERVIEW_RES_ID);
+ waitForSystemLauncherObject(SPLIT_PLACEHOLDER_RES_ID);
+ return waitForSystemLauncherObject(OVERVIEW_RES_ID);
}
case LAUNCHED_APP: {
waitUntilLauncherObjectGone(WORKSPACE_RES_ID);
waitUntilLauncherObjectGone(APPS_RES_ID);
- waitUntilLauncherObjectGone(OVERVIEW_RES_ID);
waitUntilLauncherObjectGone(WIDGETS_RES_ID);
- waitUntilLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+ waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
+ waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
if (mIgnoreTaskbarVisibility) {
return null;
}
- if (isTablet() && !isFallbackOverview()) {
- waitForLauncherObject(TASKBAR_RES_ID);
+
+ if (isTablet()) {
+ waitForSystemLauncherObject(TASKBAR_RES_ID);
} else {
- waitUntilLauncherObjectGone(TASKBAR_RES_ID);
+ waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
}
return null;
}
@@ -848,6 +866,10 @@
}
}
+ public void waitForModelQueueCleared() {
+ getTestInfo(TestProtocol.REQUEST_MODEL_QUEUE_CLEARED);
+ }
+
public void waitForLauncherInitialized() {
for (int i = 0; i < 100; ++i) {
if (getTestInfo(
@@ -968,14 +990,9 @@
checkForAnomaly(false, true);
final Point displaySize = getRealDisplaySize();
- // The swipe up to home gesture starts from inside the launcher when the user is
- // already home. Otherwise, the gesture can start inside the launcher process if the
- // taskbar is visible.
- boolean gestureStartFromLauncher = isTablet()
- ? !isLauncher3()
- || hasLauncherObject(WORKSPACE_RES_ID)
- || hasLauncherObject(TASKBAR_RES_ID)
- : isLauncherVisible();
+
+ boolean gestureStartFromLauncher =
+ isTablet() ? !isLauncher3() : isLauncherVisible();
// CLose floating views before going back to home.
swipeUpToCloseFloatingView(gestureStartFromLauncher);
@@ -1008,7 +1025,7 @@
NORMAL_STATE_ORDINAL,
!hasLauncherObject(WORKSPACE_RES_ID)
&& (hasLauncherObject(APPS_RES_ID)
- || hasLauncherObject(OVERVIEW_RES_ID)),
+ || hasSystemLauncherObject(OVERVIEW_RES_ID)),
action);
}
try (LauncherInstrumentation.Closable c1 = addContextLayer(
@@ -1064,7 +1081,8 @@
boolean isLauncherContainerVisible() {
final String[] containerResources = {WORKSPACE_RES_ID, OVERVIEW_RES_ID, APPS_RES_ID};
- return Arrays.stream(containerResources).anyMatch(r -> hasLauncherObject(r));
+ return Arrays.stream(containerResources).anyMatch(
+ r -> r.equals(OVERVIEW_RES_ID) ? hasSystemLauncherObject(r) : hasLauncherObject(r));
}
/**
@@ -1147,6 +1165,14 @@
waitUntilGoneBySelector(getOverviewObjectSelector(resId));
}
+ void waitUntilSystemLauncherObjectGone(String resId) {
+ if (is3PLauncher()) {
+ waitUntilOverviewObjectGone(resId);
+ } else {
+ waitUntilLauncherObjectGone(resId);
+ }
+ }
+
void waitUntilLauncherObjectGone(BySelector selector) {
waitUntilGoneBySelector(makeLauncherSelector(selector));
}
@@ -1277,6 +1303,11 @@
return mDevice.hasObject(getLauncherObjectSelector(resId));
}
+ private boolean hasSystemLauncherObject(String resId) {
+ return mDevice.hasObject(is3PLauncher() ? getOverviewObjectSelector(resId)
+ : getLauncherObjectSelector(resId));
+ }
+
boolean hasLauncherObject(BySelector selector) {
return mDevice.hasObject(makeLauncherSelector(selector));
}
@@ -1296,6 +1327,12 @@
}
@NonNull
+ UiObject2 waitForSystemLauncherObject(String resName) {
+ return is3PLauncher() ? waitForOverviewObject(resName)
+ : waitForLauncherObject(resName);
+ }
+
+ @NonNull
UiObject2 waitForLauncherObject(BySelector selector) {
return waitForObjectBySelector(makeLauncherSelector(selector));
}
@@ -1306,11 +1343,6 @@
}
@NonNull
- UiObject2 waitForFallbackLauncherObject(String resName) {
- return waitForObjectBySelector(getOverviewObjectSelector(resName));
- }
-
- @NonNull
UiObject2 waitForAndroidObject(String resId) {
final UiObject2 object = TestHelpers.wait(
Until.findObject(By.res(ANDROID_PACKAGE, resId)), WAIT_TIME_MS);
@@ -1347,7 +1379,7 @@
return mDevice.getLauncherPackageName();
}
- boolean isFallbackOverview() {
+ boolean is3PLauncher() {
return !getOverviewPackageName().equals(getLauncherPackageName());
}
@@ -1987,4 +2019,21 @@
mCallbackAtRunPoint.accept(runPoint);
}
}
+
+ /**
+ * Waits until a particular condition is true. Based on WaitMixin.
+ */
+ boolean waitAndGet(BooleanSupplier condition, long timeout, long interval) {
+ long startTime = SystemClock.uptimeMillis();
+
+ boolean result = condition.getAsBoolean();
+ for (long elapsedTime = 0; !result; elapsedTime = SystemClock.uptimeMillis() - startTime) {
+ if (elapsedTime >= timeout) {
+ break;
+ }
+ SystemClock.sleep(interval);
+ result = condition.getAsBoolean();
+ }
+ return result;
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Taskbar.java b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
index 0f9d5f5..6ca7f4b 100644
--- a/tests/tapl/com/android/launcher3/tapl/Taskbar.java
+++ b/tests/tapl/com/android/launcher3/tapl/Taskbar.java
@@ -52,7 +52,7 @@
try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"want to get a taskbar icon")) {
return new TaskbarAppIcon(mLauncher, mLauncher.waitForObjectInContainer(
- mLauncher.waitForLauncherObject(TASKBAR_RES_ID),
+ mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID),
AppIcon.getAppIconSelector(appName, mLauncher)));
}
}
@@ -68,7 +68,7 @@
try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"want to hide the taskbar");
LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
- mLauncher.waitForLauncherObject(TASKBAR_RES_ID);
+ mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
final long downTime = SystemClock.uptimeMillis();
Point stashTarget = new Point(
@@ -79,7 +79,7 @@
LauncherInstrumentation.log("hideTaskbar: sent down");
try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("pressed down")) {
- mLauncher.waitUntilLauncherObjectGone("taskbar_view");
+ mLauncher.waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, stashTarget,
LauncherInstrumentation.GestureScope.INSIDE);
}
@@ -97,7 +97,8 @@
LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
mLauncher.clickLauncherObject(mLauncher.waitForObjectInContainer(
- mLauncher.waitForLauncherObject(TASKBAR_RES_ID), getAllAppsButtonSelector()));
+ mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID),
+ getAllAppsButtonSelector()));
return new AllAppsFromTaskbar(mLauncher);
}
@@ -108,7 +109,7 @@
try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"want to get all taskbar icons")) {
return mLauncher.waitForObjectsInContainer(
- mLauncher.waitForLauncherObject(TASKBAR_RES_ID),
+ mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID),
AppIcon.getAnyAppIconSelector())
.stream()
.map(UiObject2::getText)