diff --git a/Android.bp b/Android.bp index 75902c8f87..39b1ae06ab 100644 --- a/Android.bp +++ b/Android.bp @@ -175,6 +175,7 @@ android_library { "androidx.preference_preference", "androidx.slice_slice-view", "androidx.cardview_cardview", + "androidx.window_window", "com.google.android.material_material", "iconloader_base", "view_capture", diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml index 7e824ec15c..a31ee80460 100644 --- a/AndroidManifest-common.xml +++ b/AndroidManifest-common.xml @@ -184,5 +184,9 @@ android:name="androidx.startup.InitializationProvider" android:authorities="${applicationId}.androidx-startup" tools:node="remove" /> + + diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig index 98dd4d68bb..defb0e66cb 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -84,6 +84,13 @@ flag { bug: "315055849" } +flag { + name: "enable_two_pane_launcher_settings" + namespace: "launcher" + description: "Enables two panel settings when on large enough displays" + bug: "204463748" +} + flag { name: "enable_shortcut_dont_suggest_app" namespace: "launcher" @@ -126,3 +133,10 @@ flag { description: "Enables asnc inflation of workspace icons" bug: "318539160" } + +flag { + name: "enable_unfold_state_animation" + namespace: "launcher" + description: "Tie unfold animation with state animation" + bug: "297057373" +} diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java index f0121970fd..e680ea90b6 100644 --- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java +++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java @@ -72,6 +72,8 @@ public class PredictionRowView private boolean mPredictionsEnabled = false; + private boolean mPredictionUiUpdatePaused = false; + public PredictionRowView(@NonNull Context context) { this(context, null); } @@ -193,7 +195,18 @@ public class PredictionRowView applyPredictionApps(); } + /** Pause the prediction row UI update */ + public void setPredictionUiUpdatePaused(boolean predictionUiUpdatePaused) { + mPredictionUiUpdatePaused = predictionUiUpdatePaused; + if (!mPredictionUiUpdatePaused) { + applyPredictionApps(); + } + } + private void applyPredictionApps() { + if (mPredictionUiUpdatePaused) { + return; + } if (getChildCount() != mNumPredictedAppsPerRow) { while (getChildCount() > mNumPredictedAppsPerRow) { removeViewAt(0); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java index 33641a4bda..7c7c426d19 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java @@ -22,6 +22,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static com.android.launcher3.BaseActivity.EVENT_DESTROYED; +import static com.android.launcher3.Flags.enableUnfoldStateAnimation; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate; @@ -405,8 +406,12 @@ public class TaskbarManager { */ private UnfoldTransitionProgressProvider getUnfoldTransitionProgressProviderForActivity( StatefulActivity activity) { - if (activity instanceof QuickstepLauncher) { - return ((QuickstepLauncher) activity).getUnfoldTransitionProgressProvider(); + if (!enableUnfoldStateAnimation()) { + if (activity instanceof QuickstepLauncher ql) { + return ql.getUnfoldTransitionProgressProvider(); + } + } else { + return SystemUiProxy.INSTANCE.get(mContext).getUnfoldTransitionProvider(); } return null; } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 74517a8c69..666d98e9c9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -51,6 +51,7 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.folder.FolderIcon; +import com.android.launcher3.folder.PreviewBackground; import com.android.launcher3.icons.ThemedIconDrawable; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; @@ -618,9 +619,11 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar super.onDraw(canvas); if (mLeaveBehindFolderIcon != null) { canvas.save(); - canvas.translate(mLeaveBehindFolderIcon.getLeft(), mLeaveBehindFolderIcon.getTop()); - mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas, - mFolderLeaveBehindColor); + canvas.translate( + mLeaveBehindFolderIcon.getLeft() + mLeaveBehindFolderIcon.getTranslationX(), + mLeaveBehindFolderIcon.getTop()); + PreviewBackground previewBackground = mLeaveBehindFolderIcon.getFolderBackground(); + previewBackground.drawLeaveBehind(canvas, mFolderLeaveBehindColor); canvas.restore(); } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index c3bcde0e8a..2e8e613595 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEAS import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED; import static com.android.app.animation.Interpolators.EMPHASIZED; +import static com.android.launcher3.Flags.enableUnfoldStateAnimation; import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.PENDING_SPLIT_SELECT_INFO; import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE; import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON; @@ -170,6 +171,8 @@ import com.android.quickstep.util.SplitSelectStateController; import com.android.quickstep.util.SplitToWorkspaceController; import com.android.quickstep.util.SplitWithKeyboardShortcutController; import com.android.quickstep.util.TISBindHelper; +import com.android.quickstep.util.unfold.LauncherUnfoldTransitionController; +import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider; import com.android.quickstep.views.FloatingTaskView; import com.android.quickstep.views.OverviewActionsView; import com.android.quickstep.views.RecentsView; @@ -367,11 +370,19 @@ public class QuickstepLauncher extends Launcher { mHotseatPredictionController.setPauseUIUpdate(getTaskbarUIController() == null); Log.d("b/318394698", "startActivitySafely being run, getTaskbarUIController is: " + getTaskbarUIController()); + PredictionRowView predictionRowView = + getAppsView().getFloatingHeaderView().findFixedRowByType(PredictionRowView.class); + // Pause the prediction row updates until the transition (if it exists) ends. + predictionRowView.setPredictionUiUpdatePaused(true); RunnableList result = super.startActivitySafely(v, intent, item); if (result == null) { mHotseatPredictionController.setPauseUIUpdate(false); + predictionRowView.setPredictionUiUpdatePaused(false); } else { - result.add(() -> mHotseatPredictionController.setPauseUIUpdate(false)); + result.add(() -> { + mHotseatPredictionController.setPauseUIUpdate(false); + predictionRowView.setPredictionUiUpdatePaused(false); + }); } return result; } @@ -468,7 +479,7 @@ public class QuickstepLauncher extends Launcher { @Override public void bindExtraContainerItems(FixedContainerItems item) { - Log.d(TAG, "Bind extra container items"); + Log.d(TAG, "Bind extra container items. ContainerId = " + item.containerId); if (item.containerId == Favorites.CONTAINER_PREDICTION) { mAllAppsPredictions = item; PredictionRowView predictionRowView = @@ -957,9 +968,17 @@ public class QuickstepLauncher extends Launcher { } private void initUnfoldTransitionProgressProvider() { - final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig(); - if (config.isEnabled()) { - initRemotelyCalculatedUnfoldAnimation(config); + if (!enableUnfoldStateAnimation()) { + final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig(); + if (config.isEnabled()) { + initRemotelyCalculatedUnfoldAnimation(config); + } + } else { + ProxyUnfoldTransitionProvider provider = + SystemUiProxy.INSTANCE.get(this).getUnfoldTransitionProvider(); + if (provider != null) { + new LauncherUnfoldTransitionController(this, provider); + } } } diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index 723af43468..56a4024c7f 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -17,6 +17,7 @@ package com.android.quickstep; import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE; +import static com.android.launcher3.Flags.enableUnfoldStateAnimation; 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.StagePosition; @@ -67,6 +68,7 @@ import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Preconditions; import com.android.quickstep.util.ActiveGestureLog; import com.android.quickstep.util.AssistUtils; +import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.RecentsAnimationControllerCompat; @@ -74,6 +76,7 @@ import com.android.systemui.shared.system.RecentsAnimationListener; 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.systemui.unfold.config.ResourceUnfoldTransitionConfig; import com.android.systemui.unfold.progress.IUnfoldAnimation; import com.android.systemui.unfold.progress.IUnfoldTransitionListener; import com.android.wm.shell.back.IBackAnimation; @@ -177,7 +180,10 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { */ private final PendingIntent mRecentsPendingIntent; - public SystemUiProxy(Context context) { + @Nullable + private final ProxyUnfoldTransitionProvider mUnfoldTransitionProvider; + + private SystemUiProxy(Context context) { mContext = context; mAsyncHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessageAsync); final Intent baseIntent = new Intent().setPackage(mContext.getPackageName()); @@ -187,6 +193,10 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { mRecentsPendingIntent = PendingIntent.getActivity(mContext, 0, baseIntent, PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT | Intent.FILL_IN_COMPONENT, options.toBundle()); + + mUnfoldTransitionProvider = + (enableUnfoldStateAnimation() && new ResourceUnfoldTransitionConfig().isEnabled()) + ? new ProxyUnfoldTransitionProvider() : null; } @Override @@ -251,7 +261,7 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { mRecentTasks = recentTasks; mBackAnimation = backAnimation; mDesktopMode = desktopMode; - mUnfoldAnimation = unfoldAnimation; + mUnfoldAnimation = enableUnfoldStateAnimation() ? null : unfoldAnimation; mDragAndDrop = dragAndDrop; linkToDeath(); // re-attach the listeners once missing due to setProxy has not been initialized yet. @@ -272,6 +282,19 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { setAssistantOverridesRequested( AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes()); mStateChangeCallbacks.forEach(Runnable::run); + + if (mUnfoldTransitionProvider != null) { + if (unfoldAnimation != null) { + try { + unfoldAnimation.setListener(mUnfoldTransitionProvider); + mUnfoldTransitionProvider.setActive(true); + } catch (RemoteException e) { + // Ignore + } + } else { + mUnfoldTransitionProvider.setActive(false); + } + } } /** @@ -1451,6 +1474,11 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { } } + @Nullable + public ProxyUnfoldTransitionProvider getUnfoldTransitionProvider() { + return mUnfoldTransitionProvider; + } + // // Recents // diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java index 1b3f5989c4..4e6b23f50c 100644 --- a/quickstep/src/com/android/quickstep/TaskIconCache.java +++ b/quickstep/src/com/android/quickstep/TaskIconCache.java @@ -191,8 +191,7 @@ public class TaskIconCache implements DisplayInfoChangeListener { entry.contentDescription = getBadgedContentDescription( activityInfo, task.key.userId, task.taskDescription); if (enableOverviewIconMenu()) { - entry.title = Utilities.trim( - activityInfo.applicationInfo.loadLabel(mContext.getPackageManager())); + entry.title = Utilities.trim(activityInfo.loadLabel(mContext.getPackageManager())); } } diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index 647ff9067d..ce6ddd8218 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -77,7 +77,6 @@ import android.view.Choreographer; import android.view.InputDevice; import android.view.InputEvent; import android.view.MotionEvent; -import android.view.SurfaceControl; import android.view.accessibility.AccessibilityManager; import androidx.annotation.BinderThread; @@ -303,15 +302,6 @@ public class TouchInteractionService extends Service { }); } - @Override - public void onNavigationBarSurface(SurfaceControl surface) { - // TODO: implement - if (surface != null) { - surface.release(); - surface = null; - } - } - @BinderThread public void onSystemUiStateChanged(int stateFlags) { MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> { diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java index 28efc9748b..50a5a832e1 100644 --- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java +++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java @@ -16,6 +16,7 @@ package com.android.quickstep.util; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported; @@ -43,6 +44,7 @@ import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.IconCache; +import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.quickstep.views.FloatingTaskView; @@ -123,7 +125,12 @@ public class SplitToWorkspaceController { intent = appInfo.intent; user = appInfo.user; bitmapInfo = appInfo.bitmap; + } else if (tag instanceof FolderInfo fi && fi.itemType == ITEM_TYPE_APP_PAIR) { + // Prompt the user to select something else by wiggling the instructions view + mController.getSplitInstructionsView().goBoing(); + return true; } else { + // Use Launcher's default click handler return false; } diff --git a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt new file mode 100644 index 0000000000..54d317d564 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util.unfold + +import android.app.Activity +import android.os.Trace +import android.view.Surface +import com.android.launcher3.Alarm +import com.android.launcher3.DeviceProfile +import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener +import com.android.launcher3.Launcher +import com.android.launcher3.anim.PendingAnimation +import com.android.launcher3.config.FeatureFlags +import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter +import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener + +/** Controls animations that are happening during unfolding foldable devices */ +class LauncherUnfoldTransitionController( + private val launcher: Launcher, + private val progressProvider: ProxyUnfoldTransitionProvider +) : OnDeviceProfileChangeListener, ActivityLifecycleCallbacksAdapter, TransitionProgressListener { + + private var isTablet: Boolean? = null + private var hasUnfoldTransitionStarted = false + private val timeoutAlarm = + Alarm().apply { + setOnAlarmListener { + onTransitionFinished() + Trace.endAsyncSection("$TAG#startedPreemptively", 0) + } + } + + init { + launcher.addOnDeviceProfileChangeListener(this) + launcher.registerActivityLifecycleCallbacks(this) + } + + override fun onActivityPaused(activity: Activity) { + progressProvider.removeCallback(this) + } + + override fun onActivityResumed(activity: Activity) { + progressProvider.addCallback(this) + } + + override fun onDeviceProfileChanged(dp: DeviceProfile) { + if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) { + return + } + + if (isTablet != null && dp.isTablet != isTablet) { + // We should preemptively start the animation only if: + // - We changed to the unfolded screen + // - SystemUI IPC connection is alive, so we won't end up in a situation that we won't + // receive transition progress events from SystemUI later because there was no + // IPC connection established (e.g. because of SystemUI crash) + // - SystemUI has not already sent unfold animation progress events. This might happen + // if Launcher was not open during unfold, in this case we receive the configuration + // change only after we went back to home screen and we don't want to start the + // animation in this case. + if (dp.isTablet && progressProvider.isActive && !hasUnfoldTransitionStarted) { + // Preemptively start the unfold animation to make sure that we have drawn + // the first frame of the animation before the screen gets unblocked + onTransitionStarted() + Trace.beginAsyncSection("$TAG#startedPreemptively", 0) + timeoutAlarm.setAlarm(PREEMPTIVE_UNFOLD_TIMEOUT_MS) + } + if (!dp.isTablet) { + // Reset unfold transition status when folded + hasUnfoldTransitionStarted = false + } + } + + isTablet = dp.isTablet + } + + override fun onTransitionStarted() { + hasUnfoldTransitionStarted = true + launcher.animationCoordinator.setAnimation( + provider = this, + factory = this::onPrepareUnfoldAnimation, + duration = + 1000L // The expected duration for the animation. Then only comes to play if we have + // to run the animation ourselves in case sysui misses the end signal + ) + timeoutAlarm.cancelAlarm() + } + + override fun onTransitionProgress(progress: Float) { + hasUnfoldTransitionStarted = true + launcher.animationCoordinator.getPlaybackController(this)?.setPlayFraction(progress) + } + + override fun onTransitionFinished() { + // Run the animation to end the animation in case it is not already at end progress. It + // will scale the duration to the remaining progress + launcher.animationCoordinator.getPlaybackController(this)?.start() + timeoutAlarm.cancelAlarm() + } + + private fun onPrepareUnfoldAnimation(anim: PendingAnimation) { + val dp = launcher.deviceProfile + val rotation = dp.displayInfo.rotation + val isVertical = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 + UnfoldAnimationBuilder.buildUnfoldAnimation( + launcher, + isVertical, + dp.displayInfo.currentSize, + anim + ) + } + + companion object { + private const val TAG = "LauncherUnfoldTransitionController" + } +} diff --git a/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt b/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt new file mode 100644 index 0000000000..83c7f72f21 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util.unfold + +import androidx.annotation.AnyThread +import androidx.annotation.FloatRange +import com.android.launcher3.util.Executors.MAIN_EXECUTOR +import com.android.systemui.unfold.UnfoldTransitionProgressProvider +import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import com.android.systemui.unfold.progress.IUnfoldTransitionListener +import com.android.systemui.unfold.progress.UnfoldRemoteFilter + +/** Receives unfold events from remote senders (System UI). */ +class ProxyUnfoldTransitionProvider : + UnfoldTransitionProgressProvider, IUnfoldTransitionListener.Stub() { + + private val listeners: MutableSet = mutableSetOf() + private val delegate = UnfoldRemoteFilter(ProcessedProgressListener()) + + private var transitionStarted = false + var isActive = false + set(value) { + field = value + if (!value) { + // Finish any active transition + onTransitionFinished() + } + } + + @AnyThread + override fun onTransitionStarted() { + MAIN_EXECUTOR.execute(delegate::onTransitionStarted) + } + + @AnyThread + override fun onTransitionProgress(progress: Float) { + MAIN_EXECUTOR.execute { delegate.onTransitionProgress(progress) } + } + + @AnyThread + override fun onTransitionFinished() { + MAIN_EXECUTOR.execute(delegate::onTransitionFinished) + } + + override fun addCallback(listener: TransitionProgressListener) { + listeners += listener + if (transitionStarted) { + // Update the listener in case there was is an active transition + listener.onTransitionStarted() + } + } + + override fun removeCallback(listener: TransitionProgressListener) { + listeners -= listener + if (transitionStarted) { + // Finish the transition if it was already running + listener.onTransitionFinished() + } + } + + override fun destroy() { + listeners.clear() + } + + private inner class ProcessedProgressListener : TransitionProgressListener { + override fun onTransitionStarted() { + if (!transitionStarted) { + transitionStarted = true + listeners.forEach(TransitionProgressListener::onTransitionStarted) + } + } + + override fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) { + listeners.forEach { it.onTransitionProgress(progress) } + } + + override fun onTransitionFinished() { + if (transitionStarted) { + transitionStarted = false + listeners.forEach(TransitionProgressListener::onTransitionFinished) + } + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt new file mode 100644 index 0000000000..d2c4728362 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util.unfold + +import android.graphics.Point +import android.view.ViewGroup +import com.android.app.animation.Interpolators.LINEAR +import com.android.app.animation.Interpolators.clampToProgress +import com.android.launcher3.CellLayout +import com.android.launcher3.Launcher +import com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY +import com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_UNFOLD_ANIMATION +import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X +import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y +import com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY +import com.android.launcher3.Workspace +import com.android.launcher3.anim.PendingAnimation +import com.android.launcher3.util.HorizontalInsettableView + +private typealias ViewGroupAction = (ViewGroup, Boolean) -> Unit + +object UnfoldAnimationBuilder { + + private val CLIP_CHILDREN: ViewGroupAction = ViewGroup::setClipChildren + private val CLIP_TO_PADDING: ViewGroupAction = ViewGroup::setClipToPadding + + data class RestoreInfo(val action: ViewGroupAction, var target: ViewGroup, var value: Boolean) + + // Percentage of the width of the quick search bar that will be reduced + // from the both sides of the bar when progress is 0 + private const val MAX_WIDTH_INSET_FRACTION = 0.04f + + // Scale factor for the whole workspace and hotseat + private const val SCALE_LAUNCHER_FROM = 0.92f + + // Translation factor for all the items on the homescreen + private const val TRANSLATION_PERCENTAGE = 0.08f + + private fun setClipChildren( + target: ViewGroup, + value: Boolean, + restoreList: MutableList + ) { + val originalValue = target.clipChildren + if (originalValue != value) { + target.clipChildren = value + restoreList.add(RestoreInfo(CLIP_CHILDREN, target, originalValue)) + } + } + + private fun setClipToPadding( + target: ViewGroup, + value: Boolean, + restoreList: MutableList + ) { + val originalValue = target.clipToPadding + if (originalValue != value) { + target.clipToPadding = value + restoreList.add(RestoreInfo(CLIP_TO_PADDING, target, originalValue)) + } + } + + private fun addChildrenAnimation( + itemsContainer: ViewGroup, + isVerticalFold: Boolean, + screenSize: Point, + anim: PendingAnimation + ) { + val tempLocation = IntArray(2) + for (i in 0 until itemsContainer.childCount) { + val child = itemsContainer.getChildAt(i) + + child.getLocationOnScreen(tempLocation) + if (isVerticalFold) { + val viewCenterX = tempLocation[0] + child.width / 2 + val distanceFromScreenCenterToViewCenter = screenSize.x / 2 - viewCenterX + anim.addFloat( + child, + VIEW_TRANSLATE_X, + distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE, + 0f, + LINEAR + ) + } else { + val viewCenterY = tempLocation[1] + child.height / 2 + val distanceFromScreenCenterToViewCenter = screenSize.y / 2 - viewCenterY + anim.addFloat( + child, + VIEW_TRANSLATE_Y, + distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE, + 0f, + LINEAR + ) + } + } + } + + /** + * Builds an animation for the unfold experience and adds it to the provided PendingAnimation + */ + fun buildUnfoldAnimation( + launcher: Launcher, + isVerticalFold: Boolean, + screenSize: Point, + anim: PendingAnimation + ) { + val restoreList = ArrayList() + val registerViews: (CellLayout) -> Unit = { cellLayout -> + setClipChildren(cellLayout, false, restoreList) + setClipToPadding(cellLayout, false, restoreList) + addChildrenAnimation(cellLayout.shortcutsAndWidgets, isVerticalFold, screenSize, anim) + } + + val workspace: Workspace<*> = launcher.workspace + val hotseat = launcher.hotseat + + // Animation icons from workspace for all orientations + workspace.forEachVisiblePage { registerViews(it as CellLayout) } + setClipChildren(workspace, false, restoreList) + setClipToPadding(workspace, true, restoreList) + + // Workspace scale + launcher.workspace.setPivotToScaleWithSelf(launcher.hotseat) + val interpolator = clampToProgress(LINEAR, 0f, 1f) + anim.addFloat( + workspace, + WORKSPACE_SCALE_PROPERTY_FACTORY[SCALE_INDEX_UNFOLD_ANIMATION], + SCALE_LAUNCHER_FROM, + 1f, + interpolator + ) + anim.addFloat( + hotseat, + HOTSEAT_SCALE_PROPERTY_FACTORY[SCALE_INDEX_UNFOLD_ANIMATION], + SCALE_LAUNCHER_FROM, + 1f, + interpolator + ) + + if (isVerticalFold) { + if (hotseat.qsb is HorizontalInsettableView) { + anim.addFloat( + hotseat.qsb as HorizontalInsettableView, + HorizontalInsettableView.HORIZONTAL_INSETS, + MAX_WIDTH_INSET_FRACTION, + 0f, + LINEAR + ) + } + registerViews(hotseat) + } + anim.addEndListener { restoreList.forEach { it.action(it.target, it.value) } } + } +} diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java index f6501f1e0f..f39a9011cd 100644 --- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java +++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java @@ -16,6 +16,8 @@ package com.android.quickstep.views; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.os.Bundle; import android.util.AttributeSet; @@ -26,9 +28,15 @@ import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.Nullable; +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; +import com.android.app.animation.Interpolators; import com.android.launcher3.LauncherState; import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.statemanager.StatefulActivity; @@ -40,7 +48,11 @@ import com.android.launcher3.statemanager.StatefulActivity; * Appears and disappears concurrently with a FloatingTaskView. */ public class SplitInstructionsView extends LinearLayout { + private static final int BOUNCE_DURATION = 250; + private static final float BOUNCE_HEIGHT = 20; + private final StatefulActivity mLauncher; + public boolean mIsCurrentlyAnimating = false; public static final FloatProperty UNFOLD = new FloatProperty<>("SplitInstructionsUnfold") { @@ -55,6 +67,19 @@ public class SplitInstructionsView extends LinearLayout { } }; + public static final FloatProperty TRANSLATE_Y = + new FloatProperty<>("SplitInstructionsTranslateY") { + @Override + public void setValue(SplitInstructionsView splitInstructionsView, float v) { + splitInstructionsView.setTranslationY(v); + } + + @Override + public Float get(SplitInstructionsView splitInstructionsView) { + return splitInstructionsView.getTranslationY(); + } + }; + public SplitInstructionsView(Context context) { this(context, null); } @@ -143,4 +168,42 @@ public class SplitInstructionsView extends LinearLayout { getMeasuredWidth() ); } + + /** + * Draws attention to the split instructions view by bouncing it up and down. + */ + public void goBoing() { + if (mIsCurrentlyAnimating) { + return; + } + + float restingY = getTranslationY(); + float bounceToY = restingY - Utilities.dpToPx(BOUNCE_HEIGHT); + PendingAnimation anim = new PendingAnimation(BOUNCE_DURATION); + // Animate the view lifting up to a higher position + anim.addFloat(this, TRANSLATE_Y, restingY, bounceToY, Interpolators.STANDARD); + + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mIsCurrentlyAnimating = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + // Create a low stiffness, medium bounce spring centering at the rest position + SpringForce spring = new SpringForce(restingY) + .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_LOW); + // Animate the view getting pulled back to rest position by the spring + SpringAnimation springAnim = new SpringAnimation(SplitInstructionsView.this, + DynamicAnimation.TRANSLATION_Y).setSpring(spring).setStartValue(bounceToY); + + springAnim.addEndListener((a, b, c, d) -> mIsCurrentlyAnimating = false); + springAnim.start(); + } + }); + + anim.buildAnim().start(); + } } diff --git a/res/layout/widgets_two_pane_sheet_recyclerview.xml b/res/layout/widgets_two_pane_sheet_recyclerview.xml index c9c855c48d..8b48abbf2d 100644 --- a/res/layout/widgets_two_pane_sheet_recyclerview.xml +++ b/res/layout/widgets_two_pane_sheet_recyclerview.xml @@ -45,24 +45,22 @@ android:background="?attr/widgetPickerPrimarySurfaceColor" android:clipToPadding="false" android:elevation="0.1dp" - android:paddingBottom="8dp" + android:paddingBottom="16dp" android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane" launcher:layout_sticky="true"> - - + \ No newline at end of file diff --git a/res/values/config.xml b/res/values/config.xml index 2a8ec2854a..5bdd7ebb36 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -252,6 +252,9 @@ + + 0.5 + 720 diff --git a/res/xml/split_configuration.xml b/res/xml/split_configuration.xml new file mode 100644 index 0000000000..531fef893b --- /dev/null +++ b/res/xml/split_configuration.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 826eeb285d..39b8de161a 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -152,6 +152,7 @@ import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; +import androidx.window.embedding.RuleController; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; @@ -580,6 +581,11 @@ public class Launcher extends StatefulActivity } setTitle(R.string.home_screen); mStartupLatencyLogger.logEnd(LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE); + + if (com.android.launcher3.Flags.enableTwoPaneLauncherSettings()) { + RuleController.getInstance(this).setRules( + RuleController.parseRules(this, R.xml.split_configuration)); + } } protected ModelCallbacks createModelCallbacks() { diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java index 069e96b456..529a8f9fac 100644 --- a/src/com/android/launcher3/model/PackageUpdatedTask.java +++ b/src/com/android/launcher3/model/PackageUpdatedTask.java @@ -353,9 +353,17 @@ public class PackageUpdatedTask extends BaseModelUpdateTask { } if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) { - Predicate removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser) - .or(ItemInfoMatcher.ofComponents(removedComponents, mUser)) - .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate()); + // This predicate is used to mark an ItemInfo for removal if its package or component + // is marked for removal. + Predicate removeAppMatch = + ItemInfoMatcher.ofPackages(removedPackages, mUser) + .or(ItemInfoMatcher.ofComponents(removedComponents, mUser)) + .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate()); + // This predicate is used to mark an app pair for removal if it contains an app marked + // for removal. + Predicate removeAppPairMatch = + ItemInfoMatcher.forAppPairMatch(removeAppMatch); + Predicate removeMatch = removeAppMatch.or(removeAppPairMatch); deleteAndBindComponentsRemoved(removeMatch, "removed because the corresponding package or component is removed. " + "mOp=" + mOp + " removedPackages=" + removedPackages.stream().collect( diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java index b6af3140fa..307411151e 100644 --- a/src/com/android/launcher3/util/ItemInfoMatcher.java +++ b/src/com/android/launcher3/util/ItemInfoMatcher.java @@ -69,6 +69,15 @@ public abstract class ItemInfoMatcher { .anyMatch(childOperator); } + /** + * Returns a matcher for items within app pairs. + */ + public static Predicate forAppPairMatch(Predicate childOperator) { + Predicate isAppPair = info -> + info instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_APP_PAIR; + return isAppPair.and(forFolderMatch(childOperator)); + } + /** * Returns a matcher for items with provided ids */ diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java index 54c9324096..66562373d1 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java @@ -56,7 +56,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395; private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry"; - private LinearLayout mSuggestedWidgetsContainer; + private FrameLayout mSuggestedWidgetsContainer; private WidgetsListHeader mSuggestedWidgetsHeader; private LinearLayout mRightPane;