From 120ae2d83f499fdadebcd0f7c59377cb5632928c Mon Sep 17 00:00:00 2001 From: Pat Manning Date: Wed, 13 Mar 2024 11:59:19 +0000 Subject: [PATCH 01/20] Set next page immediately on subsequent arrow/tab presses when navigating overivew. This allows the user to scroll as quickly as they want. Fix: 328749622 Test: manual Flag: NONE Change-Id: I493841f11407e6fb9f15fd90b5a5e55fa4ed3ad3 (cherry picked from commit a9a11b47c3bff4d3ec4a4f300e12895d3235fe95) --- quickstep/src/com/android/quickstep/views/RecentsView.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 6699147136..89328040fd 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -4140,10 +4140,10 @@ public abstract class RecentsView Date: Tue, 26 Mar 2024 21:44:02 -0700 Subject: [PATCH 02/20] Fix bug with long strings not aligning left on menu items This CL sets gravity="start" and ellipsize="end" so that longer strings in the Overview app dropdown menu are still left-aligned and ellipsized when they are longer than the container. Fixes: 330426535 Flag: N/A Test: Manual Change-Id: I1e62412c9b8be41a8dbb6b0597a69a283a56e3f0 (cherry picked from commit 6867b18db207958f5b08d53819f76d6bdf70caa7) --- quickstep/res/layout/task_view_menu_option.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quickstep/res/layout/task_view_menu_option.xml b/quickstep/res/layout/task_view_menu_option.xml index 30ab4b102e..ffe240180e 100644 --- a/quickstep/res/layout/task_view_menu_option.xml +++ b/quickstep/res/layout/task_view_menu_option.xml @@ -41,6 +41,8 @@ android:layout_marginStart="@dimen/task_menu_option_text_start_margin" android:textSize="14sp" android:textColor="?androidprv:attr/materialColorOnSurface" - android:focusable="false" /> + android:focusable="false" + android:gravity="start" + android:ellipsize="end" /> From 01cc2735375079098495d6f8a15e7d69f975074b Mon Sep 17 00:00:00 2001 From: Fengjiang Li Date: Thu, 28 Mar 2024 16:24:39 -0700 Subject: [PATCH 03/20] Add LAUNCHER_ALL_APPS_SEARCH_BACK jank instrumentation Bug: 330405993 Test: prefetto trace TBD Flag: aconfig com.android.launcher3.enable_predictive_back_gesture TEAMFOOD Change-Id: I1fb2876fb29bc360cbb8dc8c1605215f28383c3c --- .../uioverrides/states/AllAppsState.java | 11 +++++--- src/com/android/launcher3/Launcher.java | 6 ++++- .../allapps/ActivityAllAppsContainerView.java | 13 ++++++++++ .../allapps/AllAppsTransitionController.java | 25 ++++++++++++++++--- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java index 7875daee4a..2625919ec0 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java @@ -68,10 +68,13 @@ public class AllAppsState extends LauncherState { @Override public void onBackInvoked(Launcher launcher) { // In predictive back swipe, onBackInvoked() will be called after onBackStarted(). - // Because the 2nd InteractionJankMonitor.begin() will be ignore within timeout, it's safe - // to call InteractionJankMonitorWrapper.begin here. - InteractionJankMonitorWrapper.begin(launcher.getAppsView(), - Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK); + // In 3 button mode, onBackStarted() is not called but onBackInvoked() will be called. + // Thus In onBackInvoked(), we should only begin instrumenting if we didn't call + // onBackStarted() to start instrumenting CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK. + if (!InteractionJankMonitorWrapper.isInstrumenting(Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK)) { + InteractionJankMonitorWrapper.begin( + launcher.getAppsView(), Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK); + } super.onBackInvoked(launcher); } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index cfa8967c90..3273f27731 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -680,7 +680,7 @@ public class Launcher extends StatefulActivity @Override public void onBackCancelled() { - mStateManager.getState().onBackCancelled(Launcher.this); + Launcher.this.onBackCancelled(); } }; } @@ -2086,6 +2086,10 @@ public class Launcher extends StatefulActivity mStateManager.getState().onBackInvoked(this); } + protected void onBackCancelled() { + mStateManager.getState().onBackCancelled(this); + } + protected void onScreenOnChanged(boolean isOn) { // Reset AllApps to its initial state only if we are not in the middle of // processing a multi-step drop diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index 4b65b735bb..799b67b13e 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -85,6 +85,7 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.pm.UserCache; import com.android.launcher3.recyclerview.AllAppsRecyclerViewPool; import com.android.launcher3.util.ItemInfoMatcher; +import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.Themes; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.BaseDragLayer; @@ -1366,6 +1367,18 @@ public class ActivityAllAppsContainerView invalidateHeader(); } + /** + * Set {@link Animator.AnimatorListener} on {@link mAllAppsTransitionController} to observe + * animation of backing out of all apps search view to all apps view. + */ + public void setAllAppsSearchBackAnimatorListener(Animator.AnimatorListener listener) { + Preconditions.assertNotNull(mAllAppsTransitionController); + if (mAllAppsTransitionController == null) { + return; + } + mAllAppsTransitionController.setAllAppsSearchBackAnimationListener(listener); + } + public void setScrimView(ScrimView scrimView) { mScrimView = scrimView; } diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java index 63f6227941..a4d1dc16c3 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -44,6 +44,7 @@ import android.view.View; import android.view.animation.Interpolator; import androidx.annotation.FloatRange; +import androidx.annotation.Nullable; import com.android.app.animation.Interpolators; import com.android.launcher3.DeviceProfile; @@ -167,6 +168,8 @@ public class AllAppsTransitionController private final AnimatedFloat mAllAppScale = new AnimatedFloat(this::onScaleProgressChanged); private final int mNavScrimFlag; + @Nullable private Animator.AnimatorListener mAllAppsSearchBackAnimationListener; + private boolean mIsVerticalLayout; // Animation in this class is controlled by a single variable {@link mProgress}. @@ -312,11 +315,25 @@ public class AllAppsTransitionController } } - /** Animate all apps view to 1f scale. */ + /** Set {@link Animator.AnimatorListener} for scaling all apps scale to 1 animation. */ + public void setAllAppsSearchBackAnimationListener(Animator.AnimatorListener listener) { + mAllAppsSearchBackAnimationListener = listener; + } + + /** + * Animate all apps view to 1f scale. This is called when backing (exiting) from all apps + * search view to all apps view. + */ public void animateAllAppsToNoScale() { - mAllAppScale.animateToValue(1f) - .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS) - .start(); + if (mAllAppScale.isAnimating()) { + return; + } + Animator animator = mAllAppScale.animateToValue(1f) + .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS); + if (mAllAppsSearchBackAnimationListener != null) { + animator.addListener(mAllAppsSearchBackAnimationListener); + } + animator.start(); } /** From 7ae51788dae396803c184b58434a485d8445978e Mon Sep 17 00:00:00 2001 From: Shamali P Date: Fri, 22 Mar 2024 13:40:35 +0000 Subject: [PATCH 04/20] Update the WidgetPickerActivity to display recommendations for hub host - Accepts a ui_surface param of format "widgets{_hub}" and existing widgets on the surface to be excluded from predictions - Refactored the widgets prediction update task to extract reusable logic that maps the predictions to widget items and reused it. http://screencast/cast/NjE1MTA5MDI0NzU2NTMxMnwzMGE3NTMwNi1hZg Bug: 326092660 Test: WidgetsPredictionHelperTest and see screencast above. Flag: N/A Change-Id: I6ceeb752c167893bab4ed496cedc5e8081e1b950 --- .../launcher3/WidgetPickerActivity.java | 83 ++++++- .../model/WidgetPredictionsRequester.java | 233 ++++++++++++++++++ .../model/WidgetsPredictionsRequesterTest.kt | 221 +++++++++++++++++ 3 files changed, 529 insertions(+), 8 deletions(-) create mode 100644 quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java create mode 100644 quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java index 8c4db4a569..23cb8e9baa 100644 --- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java +++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java @@ -35,23 +35,31 @@ import android.view.WindowInsetsController; import android.view.WindowManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.launcher3.dragndrop.SimpleDragLayer; import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.model.WidgetPredictionsRequester; import com.android.launcher3.model.WidgetsModel; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.popup.PopupDataProvider; +import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.widget.BaseWidgetSheet; import com.android.launcher3.widget.WidgetCell; import com.android.launcher3.widget.model.WidgetsListBaseEntry; +import com.android.launcher3.widget.model.WidgetsListHeaderEntry; import com.android.launcher3.widget.picker.WidgetsFullSheet; import java.util.ArrayList; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; /** An Activity that can host Launcher's widget picker. */ public class WidgetPickerActivity extends BaseActivity { private static final String TAG = "WidgetPickerActivity"; - /** * Name of the extra that indicates that a widget being dragged. * @@ -64,14 +72,33 @@ public class WidgetPickerActivity extends BaseActivity { // the intent, then widgets will not be filtered for size. private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"; private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"; - + /** + * Widgets currently added by the user in the UI surface. + *

This allows widget picker to exclude existing widgets from suggestions.

+ */ + private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets"; + /** + * A unique identifier of the surface hosting the widgets; + *

"widgets" is reserved for home screen surface.

+ *

"widgets_hub" is reserved for glanceable hub surface.

+ */ + private static final String EXTRA_UI_SURFACE = "ui_surface"; + private static final Pattern UI_SURFACE_PATTERN = + Pattern.compile("^(widgets|widgets_hub)$"); private SimpleDragLayer mDragLayer; private WidgetsModel mModel; + private LauncherAppState mApp; + private WidgetPredictionsRequester mWidgetPredictionsRequester; private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {}); private int mDesiredWidgetWidth; private int mDesiredWidgetHeight; private int mWidgetCategoryFilter; + @Nullable + private String mUiSurface; + // Widgets existing on the host surface. + @NonNull + private List mAddedWidgets = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { @@ -80,9 +107,8 @@ public class WidgetPickerActivity extends BaseActivity { getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER); - LauncherAppState app = LauncherAppState.getInstance(this); - InvariantDeviceProfile idp = app.getInvariantDeviceProfile(); - + mApp = LauncherAppState.getInstance(this); + InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile(); mDeviceProfile = idp.getDeviceProfile(this); mModel = new WidgetsModel(); @@ -97,6 +123,11 @@ public class WidgetPickerActivity extends BaseActivity { widgetSheet.disableNavBarScrim(true); widgetSheet.addOnCloseListener(this::finish); + parseIntentExtras(); + refreshAndBindWidgets(); + } + + private void parseIntentExtras() { // A value of 0 for either size means that no filtering will occur in that dimension. If // both values are 0, then no size filtering will occur. mDesiredWidgetWidth = @@ -108,7 +139,15 @@ public class WidgetPickerActivity extends BaseActivity { mWidgetCategoryFilter = getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0); - refreshAndBindWidgets(); + String uiSurfaceParam = getIntent().getStringExtra(EXTRA_UI_SURFACE); + if (uiSurfaceParam != null && UI_SURFACE_PATTERN.matcher(uiSurfaceParam).matches()) { + mUiSurface = uiSurfaceParam; + } + ArrayList addedWidgets = getIntent().getParcelableArrayListExtra( + EXTRA_ADDED_APP_WIDGETS, AppWidgetProviderInfo.class); + if (addedWidgets != null) { + mAddedWidgets = addedWidgets; + } } @NonNull @@ -179,11 +218,12 @@ public class WidgetPickerActivity extends BaseActivity { }; } + /** Updates the model with widgets and provides them after applying the provided filter. */ private void refreshAndBindWidgets() { MODEL_EXECUTOR.execute(() -> { LauncherAppState app = LauncherAppState.getInstance(this); mModel.update(app, null); - final ArrayList widgets = + final List allWidgets = mModel.getFilteredWidgetsListForPicker( app.getContext(), /*widgetItemFilter=*/ widget -> { @@ -193,10 +233,37 @@ public class WidgetPickerActivity extends BaseActivity { return verdict.isAcceptable; } ); - MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets)); + bindWidgets(allWidgets); + if (mUiSurface != null) { + Map> allWidgetsMap = allWidgets.stream() + .filter(WidgetsListHeaderEntry.class::isInstance) + .collect(Collectors.toMap( + entry -> PackageUserKey.fromPackageItemInfo(entry.mPkgItem), + entry -> entry.mWidgets) + ); + mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(), + mUiSurface, allWidgetsMap); + mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets); + } }); } + private void bindWidgets(List widgets) { + MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets)); + } + + private void bindRecommendedWidgets(List recommendedWidgets) { + MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets)); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mWidgetPredictionsRequester != null) { + mWidgetPredictionsRequester.clear(); + } + } + private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) { final AppWidgetProviderInfo info = widget.widgetInfo; if (info == null) { diff --git a/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java new file mode 100644 index 0000000000..84313965e6 --- /dev/null +++ b/quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model; + +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; + +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionManager; +import android.app.prediction.AppPredictor; +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.AppTargetId; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; + +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.widget.PendingAddWidgetInfo; +import com.android.launcher3.widget.picker.WidgetRecommendationCategoryProvider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Works with app predictor to fetch and process widget predictions displayed in a standalone + * widget picker activity for a UI surface. + */ +public class WidgetPredictionsRequester { + private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20; + private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets"; + + @Nullable + private AppPredictor mAppPredictor; + private final Context mContext; + @NonNull + private final String mUiSurface; + @NonNull + private final Map> mAllWidgets; + + public WidgetPredictionsRequester(Context context, @NonNull String uiSurface, + @NonNull Map> allWidgets) { + mContext = context; + mUiSurface = uiSurface; + mAllWidgets = Collections.unmodifiableMap(allWidgets); + } + + /** + * Requests predictions from the app predictions manager and registers the provided callback to + * receive updates when predictions are available. + * + * @param existingWidgets widgets that are currently added to the surface; + * @param callback consumer of prediction results to be called when predictions are + * available + */ + public void request(List existingWidgets, + Consumer> callback) { + Bundle bundle = buildBundleForPredictionSession(existingWidgets, mUiSurface); + Predicate filter = notOnUiSurfaceFilter(existingWidgets); + + MODEL_EXECUTOR.execute(() -> { + clear(); + AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class); + if (apm == null) { + return; + } + + mAppPredictor = apm.createAppPredictionSession( + new AppPredictionContext.Builder(mContext) + .setUiSurface(mUiSurface) + .setExtras(bundle) + .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION) + .build()); + mAppPredictor.registerPredictionUpdates(MODEL_EXECUTOR, + targets -> bindPredictions(targets, filter, callback)); + mAppPredictor.requestPredictionUpdate(); + }); + } + + /** + * Returns a bundle that can be passed in a prediction session + * + * @param addedWidgets widgets that are already added by the user in the ui surface + * @param uiSurface a unique identifier of the surface hosting widgets; format + * "widgets_xx"; note - "widgets" is reserved for home screen surface. + */ + @VisibleForTesting + static Bundle buildBundleForPredictionSession(List addedWidgets, + String uiSurface) { + Bundle bundle = new Bundle(); + ArrayList addedAppTargetEvents = new ArrayList<>(); + for (AppWidgetProviderInfo info : addedWidgets) { + ComponentName componentName = info.provider; + AppTargetEvent appTargetEvent = buildAppTargetEvent(uiSurface, info, componentName); + addedAppTargetEvents.add(appTargetEvent); + } + bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, addedAppTargetEvents); + return bundle; + } + + /** + * Builds the AppTargetEvent for added widgets in a form that can be passed to the widget + * predictor. + * Also see {@link PredictionHelper} + */ + private static AppTargetEvent buildAppTargetEvent(String uiSurface, AppWidgetProviderInfo info, + ComponentName componentName) { + AppTargetId appTargetId = new AppTargetId("widget:" + componentName.getPackageName()); + AppTarget appTarget = new AppTarget.Builder(appTargetId, componentName.getPackageName(), + /*user=*/ info.getProfile()).setClassName(componentName.getClassName()).build(); + return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN) + .setLaunchLocation(uiSurface).build(); + } + + /** + * Returns a filter to match {@link WidgetItem}s that don't exist on the UI surface. + */ + @NonNull + @VisibleForTesting + static Predicate notOnUiSurfaceFilter( + List existingWidgets) { + Set existingComponentKeys = existingWidgets.stream().map( + widget -> new ComponentKey(widget.provider, widget.getProfile())).collect( + Collectors.toSet()); + return widgetItem -> !existingComponentKeys.contains(widgetItem); + } + + /** Provides the predictions returned by the predictor to the registered callback. */ + @WorkerThread + private void bindPredictions(List targets, Predicate filter, + Consumer> callback) { + List filteredPredictions = filterPredictions(targets, mAllWidgets, filter); + List mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions); + + MAIN_EXECUTOR.execute(() -> callback.accept(mappedPredictions)); + } + + /** + * Applies the provided filter (e.g. widgets not on workspace) on the predictions returned by + * the predictor. + */ + @VisibleForTesting + static List filterPredictions(List predictions, + Map> allWidgets, Predicate filter) { + List servicePredictedItems = new ArrayList<>(); + List localFilteredWidgets = new ArrayList<>(); + + for (AppTarget prediction : predictions) { + List widgetsInPackage = allWidgets.get( + new PackageUserKey(prediction.getPackageName(), prediction.getUser())); + if (widgetsInPackage == null || widgetsInPackage.isEmpty()) { + continue; + } + String className = prediction.getClassName(); + if (!TextUtils.isEmpty(className)) { + WidgetItem item = widgetsInPackage.stream() + .filter(w -> className.equals(w.componentName.getClassName())) + .filter(filter) + .findFirst().orElse(null); + if (item != null) { + servicePredictedItems.add(item); + continue; + } + } + // No widget was added by the service, try local filtering + widgetsInPackage.stream().filter(filter).findFirst() + .ifPresent(localFilteredWidgets::add); + } + if (servicePredictedItems.isEmpty()) { + servicePredictedItems.addAll(localFilteredWidgets); + } + + return servicePredictedItems; + } + + /** + * Converts the list of {@link WidgetItem}s to the list of {@link ItemInfo}s. + */ + private List mapWidgetItemsToItemInfo(List widgetItems) { + List items; + if (enableCategorizedWidgetSuggestions()) { + WidgetRecommendationCategoryProvider categoryProvider = + WidgetRecommendationCategoryProvider.newInstance(mContext); + items = widgetItems.stream() + .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION, + categoryProvider.getWidgetRecommendationCategory(mContext, it))) + .collect(Collectors.toList()); + } else { + items = widgetItems.stream().map(it -> new PendingAddWidgetInfo(it.widgetInfo, + CONTAINER_WIDGETS_PREDICTION)).collect(Collectors.toList()); + } + return items; + } + + /** Cleans up any open prediction sessions. */ + public void clear() { + if (mAppPredictor != null) { + mAppPredictor.destroy(); + mAppPredictor = null; + } + } +} diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt new file mode 100644 index 0000000000..5c7b4aba4d --- /dev/null +++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.model + +import android.app.prediction.AppTarget +import android.app.prediction.AppTargetEvent +import android.app.prediction.AppTargetId +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.content.Context +import android.os.Process.myUserHandle +import android.os.UserHandle +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.DeviceProfile +import com.android.launcher3.InvariantDeviceProfile +import com.android.launcher3.LauncherAppState +import com.android.launcher3.icons.IconCache +import com.android.launcher3.model.WidgetPredictionsRequester.buildBundleForPredictionSession +import com.android.launcher3.model.WidgetPredictionsRequester.filterPredictions +import com.android.launcher3.model.WidgetPredictionsRequester.notOnUiSurfaceFilter +import com.android.launcher3.util.ActivityContextWrapper +import com.android.launcher3.util.PackageUserKey +import com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo +import com.android.launcher3.widget.LauncherAppWidgetProviderInfo +import com.google.common.truth.Truth.assertThat +import java.util.function.Predicate +import junit.framework.Assert.assertNotNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +class WidgetsPredictionsRequesterTest { + + private lateinit var mUserHandle: UserHandle + private lateinit var context: Context + private lateinit var deviceProfile: DeviceProfile + private lateinit var testInvariantProfile: InvariantDeviceProfile + + private lateinit var widget1aInfo: AppWidgetProviderInfo + private lateinit var widget1bInfo: AppWidgetProviderInfo + private lateinit var widget2Info: AppWidgetProviderInfo + + private lateinit var widgetItem1a: WidgetItem + private lateinit var widgetItem1b: WidgetItem + private lateinit var widgetItem2: WidgetItem + + private lateinit var allWidgets: Map> + + @Mock private lateinit var iconCache: IconCache + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mUserHandle = myUserHandle() + context = ActivityContextWrapper(ApplicationProvider.getApplicationContext()) + testInvariantProfile = LauncherAppState.getIDP(context) + deviceProfile = testInvariantProfile.getDeviceProfile(context).copy(context) + + widget1aInfo = + createAppWidgetProviderInfo( + ComponentName.createRelative(APP_1_PACKAGE_NAME, APP_1_PROVIDER_A_CLASS_NAME) + ) + widget1bInfo = + createAppWidgetProviderInfo( + ComponentName.createRelative(APP_1_PACKAGE_NAME, APP_1_PROVIDER_B_CLASS_NAME) + ) + widgetItem1a = createWidgetItem(widget1aInfo) + widgetItem1b = createWidgetItem(widget1bInfo) + + widget2Info = + createAppWidgetProviderInfo( + ComponentName.createRelative(APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME) + ) + widgetItem2 = createWidgetItem(widget2Info) + + allWidgets = + mapOf( + PackageUserKey(APP_1_PACKAGE_NAME, mUserHandle) to + listOf(widgetItem1a, widgetItem1b), + PackageUserKey(APP_2_PACKAGE_NAME, mUserHandle) to listOf(widgetItem2), + ) + } + + @Test + fun buildBundleForPredictionSession_includesAddedAppWidgets() { + val existingWidgets = arrayListOf(widget1aInfo, widget1bInfo, widget2Info) + + val bundle = buildBundleForPredictionSession(existingWidgets, TEST_UI_SURFACE) + val addedWidgetsBundleExtra = + bundle.getParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, AppTarget::class.java) + + assertNotNull(addedWidgetsBundleExtra) + assertThat(addedWidgetsBundleExtra) + .containsExactly( + buildExpectedAppTargetEvent( + /*pkg=*/ APP_1_PACKAGE_NAME, + /*providerClassName=*/ APP_1_PROVIDER_A_CLASS_NAME, + /*user=*/ mUserHandle + ), + buildExpectedAppTargetEvent( + /*pkg=*/ APP_1_PACKAGE_NAME, + /*providerClassName=*/ APP_1_PROVIDER_B_CLASS_NAME, + /*user=*/ mUserHandle + ), + buildExpectedAppTargetEvent( + /*pkg=*/ APP_2_PACKAGE_NAME, + /*providerClassName=*/ APP_2_PROVIDER_1_CLASS_NAME, + /*user=*/ mUserHandle + ) + ) + } + + @Test + fun filterPredictions_notOnUiSurfaceFilter_returnsOnlyEligiblePredictions() { + val widgetsAlreadyOnSurface = arrayListOf(widget1bInfo) + val filter: Predicate = notOnUiSurfaceFilter(widgetsAlreadyOnSurface) + + val predictions = + listOf( + // already on surface + AppTarget( + AppTargetId(APP_1_PACKAGE_NAME), + APP_1_PACKAGE_NAME, + APP_1_PROVIDER_B_CLASS_NAME, + mUserHandle + ), + // eligible + AppTarget( + AppTargetId(APP_2_PACKAGE_NAME), + APP_2_PACKAGE_NAME, + APP_2_PROVIDER_1_CLASS_NAME, + mUserHandle + ) + ) + + // only 2 was eligible + assertThat(filterPredictions(predictions, allWidgets, filter)).containsExactly(widgetItem2) + } + + @Test + fun filterPredictions_appPredictions_returnsWidgetFromPackage() { + val widgetsAlreadyOnSurface = arrayListOf(widget1bInfo) + val filter: Predicate = notOnUiSurfaceFilter(widgetsAlreadyOnSurface) + + val predictions = + listOf( + AppTarget( + AppTargetId(APP_1_PACKAGE_NAME), + APP_1_PACKAGE_NAME, + "$APP_1_PACKAGE_NAME.SomeActivity", + mUserHandle + ), + AppTarget( + AppTargetId(APP_2_PACKAGE_NAME), + APP_2_PACKAGE_NAME, + "$APP_2_PACKAGE_NAME.SomeActivity2", + mUserHandle + ), + ) + + assertThat(filterPredictions(predictions, allWidgets, filter)) + .containsExactly(widgetItem1a, widgetItem2) + } + + private fun createWidgetItem( + providerInfo: AppWidgetProviderInfo, + ): WidgetItem { + val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo) + return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context) + } + + companion object { + const val TEST_UI_SURFACE = "widgets_test" + const val BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets" + + const val APP_1_PACKAGE_NAME = "com.example.app1" + const val APP_1_PROVIDER_A_CLASS_NAME = "app1Provider1" + const val APP_1_PROVIDER_B_CLASS_NAME = "app1Provider2" + + const val APP_2_PACKAGE_NAME = "com.example.app2" + const val APP_2_PROVIDER_1_CLASS_NAME = "app2Provider1" + + const val TEST_PACKAGE = "pkg" + + private fun buildExpectedAppTargetEvent( + pkg: String, + providerClassName: String, + userHandle: UserHandle + ): AppTargetEvent { + val appTarget = + AppTarget.Builder( + /*id=*/ AppTargetId("widget:$pkg"), + /*packageName=*/ pkg, + /*user=*/ userHandle + ) + .setClassName(providerClassName) + .build() + return AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN) + .setLaunchLocation(TEST_UI_SURFACE) + .build() + } + } +} From f151bfdd53d23f0c7ad0c4a15d0cb9c028e365bf Mon Sep 17 00:00:00 2001 From: Johannes Gallmann Date: Tue, 2 Apr 2024 17:35:45 +0200 Subject: [PATCH 05/20] Add vsync id to predictive back transactions Bug: 331808052 Flag: ACONFIG com.android.window.flags.predictive_back_system_anims TRUNKFOOD Test: Manual, i.e. recorded perfetto trace and verified that missedFrames metrics showed up Change-Id: I3dc5900ddedcb70467de7667f1555862dee5b45d --- .../quickstep/LauncherBackAnimationController.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java index 2d25295402..fa425f14c9 100644 --- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java +++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java @@ -39,6 +39,7 @@ import android.os.Handler; import android.os.RemoteException; import android.util.Log; import android.util.Pair; +import android.view.Choreographer; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationTarget; @@ -302,7 +303,7 @@ public class LauncherBackAnimationController { if (mScrimLayer == null) { addScrimLayer(); } - mTransaction.apply(); + applyTransaction(); } private void setLauncherTargetViewVisible(boolean isVisible) { @@ -342,7 +343,8 @@ public class LauncherBackAnimationController { return; } if (mScrimLayer.isValid()) { - mTransaction.remove(mScrimLayer).apply(); + mTransaction.remove(mScrimLayer); + applyTransaction(); } mScrimLayer = null; } @@ -396,7 +398,11 @@ public class LauncherBackAnimationController { mTransaction.setWindowCrop(mBackTarget.leash, mStartRect); mTransaction.setCornerRadius(mBackTarget.leash, cornerRadius); } + applyTransaction(); + } + private void applyTransaction() { + mTransaction.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); mTransaction.apply(); } @@ -511,7 +517,7 @@ public class LauncherBackAnimationController { float value = (Float) animation.getAnimatedValue(); if (mScrimLayer != null && mScrimLayer.isValid()) { mTransaction.setAlpha(mScrimLayer, value * mScrimAlpha); - mTransaction.apply(); + applyTransaction(); } }); mScrimAlphaAnimator.addListener(new AnimatorListenerAdapter() { From 9e6e3562f0ea54d9c976e35b3341d41411cff3d5 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Mon, 1 Apr 2024 15:36:34 -0700 Subject: [PATCH 06/20] Using IHomeTransitionListener for listening for Launcher state when taskbar recreates IHomeTransitionListener was only used when the visibility state changes, but not when the taskbar is recreated during rotation. Instead we create a static listener to keep track of current home visibility state Bug: 331947346 Bug: 331947116 Flag: aconfig use_activity_overlay staging Test: Manual Change-Id: Icf613f7fc4f78e3f76a600202687b069d53a16dd --- .../launcher3/HomeTransitionController.java | 55 ---------------- .../taskbar/LauncherTaskbarUIController.java | 18 ++++- .../TaskbarLauncherStateController.java | 3 +- .../uioverrides/QuickstepLauncher.java | 14 +--- .../android/quickstep/HomeVisibilityState.kt | 66 +++++++++++++++++++ .../quickstep/LauncherActivityInterface.java | 14 +++- .../com/android/quickstep/SystemUiProxy.java | 26 ++------ 7 files changed, 101 insertions(+), 95 deletions(-) delete mode 100644 quickstep/src/com/android/launcher3/HomeTransitionController.java create mode 100644 quickstep/src/com/android/quickstep/HomeVisibilityState.kt diff --git a/quickstep/src/com/android/launcher3/HomeTransitionController.java b/quickstep/src/com/android/launcher3/HomeTransitionController.java deleted file mode 100644 index c4a2e9e17e..0000000000 --- a/quickstep/src/com/android/launcher3/HomeTransitionController.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.launcher3; - -import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; - -import androidx.annotation.Nullable; - -import com.android.launcher3.uioverrides.QuickstepLauncher; -import com.android.quickstep.SystemUiProxy; -import com.android.wm.shell.shared.IHomeTransitionListener; - -/** - * Controls launcher response to home activity visibility changing. - */ -public class HomeTransitionController { - - @Nullable private QuickstepLauncher mLauncher; - @Nullable private IHomeTransitionListener mHomeTransitionListener; - - public void registerHomeTransitionListener(QuickstepLauncher launcher) { - mLauncher = launcher; - mHomeTransitionListener = new IHomeTransitionListener.Stub() { - @Override - public void onHomeVisibilityChanged(boolean isVisible) { - MAIN_EXECUTOR.execute(() -> { - if (mLauncher != null && mLauncher.getTaskbarUIController() != null) { - mLauncher.getTaskbarUIController().onLauncherVisibilityChanged(isVisible); - } - }); - } - }; - - SystemUiProxy.INSTANCE.get(mLauncher).setHomeTransitionListener(mHomeTransitionListener); - } - - public void unregisterHomeTransitionListener() { - SystemUiProxy.INSTANCE.get(mLauncher).setHomeTransitionListener(null); - mHomeTransitionListener = null; - mLauncher = null; - } -} diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java index a59aeada5f..def6287d2b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -35,6 +35,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Flags; import com.android.launcher3.LauncherState; import com.android.launcher3.QuickstepTransitionManager; import com.android.launcher3.R; @@ -49,8 +50,10 @@ import com.android.launcher3.uioverrides.QuickstepLauncher; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.OnboardingPrefs; +import com.android.quickstep.HomeVisibilityState; import com.android.quickstep.LauncherActivityInterface; import com.android.quickstep.RecentsAnimationCallbacks; +import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.TISBindHelper; import com.android.quickstep.views.RecentsView; @@ -79,6 +82,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController { AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max); private final QuickstepLauncher mLauncher; + private final HomeVisibilityState mHomeState; private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener = dp -> { @@ -87,6 +91,8 @@ public class LauncherTaskbarUIController extends TaskbarUIController { mControllers.taskbarViewController.onRotationChanged(dp); } }; + private final HomeVisibilityState.VisibilityChangeListener mVisibilityChangeListener = + this::onLauncherVisibilityChanged; // Initialized in init. private final TaskbarLauncherStateController @@ -94,6 +100,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController { public LauncherTaskbarUIController(QuickstepLauncher launcher) { mLauncher = launcher; + mHomeState = SystemUiProxy.INSTANCE.get(mLauncher).getHomeVisibilityState(); } @Override @@ -104,8 +111,11 @@ public class LauncherTaskbarUIController extends TaskbarUIController { mControllers.getSharedState().sysuiStateFlags); mLauncher.setTaskbarUIController(this); - - onLauncherVisibilityChanged(mLauncher.hasBeenResumed(), true /* fromInit */); + mHomeState.addListener(mVisibilityChangeListener); + onLauncherVisibilityChanged( + Flags.useActivityOverlay() + ? mHomeState.isHomeVisible() : mLauncher.hasBeenResumed(), + true /* fromInit */); onStashedInAppChanged(mLauncher.getDeviceProfile()); mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); @@ -129,6 +139,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController { mLauncher.setTaskbarUIController(null); mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener); + mHomeState.removeListener(mVisibilityChangeListener); updateTaskTransitionSpec(true); } @@ -234,7 +245,8 @@ public class LauncherTaskbarUIController extends TaskbarUIController { @Override public void refreshResumedState() { - onLauncherVisibilityChanged(mLauncher.hasBeenResumed()); + onLauncherVisibilityChanged(Flags.useActivityOverlay() + ? mHomeState.isHomeVisible() : mLauncher.hasBeenResumed()); } @Override diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java index 8d481543c8..1a120da8b2 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java @@ -656,7 +656,8 @@ public class TaskbarLauncherStateController { * Returns if the current Launcher state has hotseat on top of other elemnets. */ public boolean isInHotseatOnTopStates() { - return mLauncherState != LauncherState.ALL_APPS; + return mLauncherState != LauncherState.ALL_APPS + && !mLauncher.getWorkspace().isOverlayShown(); } boolean isInOverview() { diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index b49c7525ea..8b923ada30 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -62,8 +62,8 @@ import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MOD import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback; import static com.android.quickstep.util.SplitAnimationTimings.TABLET_HOME_TO_SPLIT; -import static com.android.window.flags.Flags.enableDesktopWindowingMode; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY; +import static com.android.window.flags.Flags.enableDesktopWindowingMode; import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50; import android.animation.Animator; @@ -105,7 +105,6 @@ import com.android.app.viewcapture.SettingsAwareViewCapture; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Flags; -import com.android.launcher3.HomeTransitionController; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings.Favorites; @@ -245,8 +244,6 @@ public class QuickstepLauncher extends Launcher { private boolean mIsPredictiveBackToHomeInProgress; - private HomeTransitionController mHomeTransitionController; - @Override protected void setupViews() { super.setupViews(); @@ -277,11 +274,6 @@ public class QuickstepLauncher extends Launcher { mAppTransitionManager.registerRemoteAnimations(); mAppTransitionManager.registerRemoteTransitions(); - if (FeatureFlags.enableHomeTransitionListener()) { - mHomeTransitionController = new HomeTransitionController(); - mHomeTransitionController.registerHomeTransitionListener(this); - } - mTISBindHelper = new TISBindHelper(this, this::onTISConnected); mDepthController = new DepthController(this); mDesktopVisibilityController = new DesktopVisibilityController(this); @@ -523,10 +515,6 @@ public class QuickstepLauncher extends Launcher { mLauncherUnfoldAnimationController.onDestroy(); } - if (mHomeTransitionController != null) { - mHomeTransitionController.unregisterHomeTransitionListener(); - } - if (mDesktopVisibilityController != null) { mDesktopVisibilityController.unregisterSystemUiListener(); } diff --git a/quickstep/src/com/android/quickstep/HomeVisibilityState.kt b/quickstep/src/com/android/quickstep/HomeVisibilityState.kt new file mode 100644 index 0000000000..241e16d111 --- /dev/null +++ b/quickstep/src/com/android/quickstep/HomeVisibilityState.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep + +import android.os.RemoteException +import android.util.Log +import com.android.launcher3.config.FeatureFlags +import com.android.launcher3.util.Executors +import com.android.wm.shell.shared.IHomeTransitionListener.Stub +import com.android.wm.shell.shared.IShellTransitions + +/** Class to track visibility state of Launcher */ +class HomeVisibilityState { + + var isHomeVisible = true + private set + + private var listeners = mutableSetOf() + + fun addListener(l: VisibilityChangeListener) = listeners.add(l) + + fun removeListener(l: VisibilityChangeListener) = listeners.remove(l) + + fun init(transitions: IShellTransitions?) { + if (!FeatureFlags.enableHomeTransitionListener()) return + try { + transitions?.setHomeTransitionListener( + object : Stub() { + override fun onHomeVisibilityChanged(isVisible: Boolean) { + Executors.MAIN_EXECUTOR.execute { + isHomeVisible = isVisible + listeners.forEach { it.onHomeVisibilityChanged(isVisible) } + } + } + } + ) + } catch (e: RemoteException) { + Log.w(TAG, "Failed call setHomeTransitionListener", e) + } + } + + interface VisibilityChangeListener { + fun onHomeVisibilityChanged(isVisible: Boolean) + } + + override fun toString() = "{HomeVisibilityState isHomeVisible=$isHomeVisible}" + + companion object { + + private const val TAG = "HomeVisibilityState" + } +} diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java index 97c48e6327..7c17e4ee7a 100644 --- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java +++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java @@ -35,6 +35,7 @@ import androidx.annotation.Nullable; import androidx.annotation.UiThread; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Flags; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.LauncherInitListener; @@ -206,8 +207,17 @@ public final class LauncherActivityInterface extends @UiThread private Launcher getVisibleLauncher() { Launcher launcher = getCreatedActivity(); - return (launcher != null) && launcher.isStarted() - && (isInLiveTileMode() || launcher.hasBeenResumed()) ? launcher : null; + if (launcher == null) { + return null; + } + if (launcher.isStarted() && (isInLiveTileMode() || launcher.hasBeenResumed())) { + return launcher; + } + if (Flags.useActivityOverlay() + && SystemUiProxy.INSTANCE.get(launcher).getHomeVisibilityState().isHomeVisible()) { + return launcher; + } + return null; } @Override diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index b6272dad87..1521b02b7a 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -63,7 +63,6 @@ import androidx.annotation.WorkerThread; import com.android.internal.logging.InstanceId; import com.android.internal.util.ScreenshotRequest; import com.android.internal.view.AppearanceRegion; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Preconditions; import com.android.quickstep.util.ActiveGestureLog; @@ -91,7 +90,6 @@ import com.android.wm.shell.draganddrop.IDragAndDrop; import com.android.wm.shell.onehanded.IOneHanded; import com.android.wm.shell.recents.IRecentTasks; import com.android.wm.shell.recents.IRecentTasksListener; -import com.android.wm.shell.shared.IHomeTransitionListener; import com.android.wm.shell.shared.IShellTransitions; import com.android.wm.shell.splitscreen.ISplitScreen; import com.android.wm.shell.splitscreen.ISplitScreenListener; @@ -157,7 +155,7 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { private IOnBackInvokedCallback mBackToLauncherCallback; private IRemoteAnimationRunner mBackToLauncherRunner; private IDragAndDrop mDragAndDrop; - private IHomeTransitionListener mHomeTransitionListener; + private final HomeVisibilityState mHomeVisibilityState = new HomeVisibilityState(); // Used to dedupe calls to SystemUI private int mLastShelfHeight; @@ -269,7 +267,7 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { setBubblesListener(mBubblesListener); registerSplitScreenListener(mSplitScreenListener); registerSplitSelectListener(mSplitSelectListener); - setHomeTransitionListener(mHomeTransitionListener); + mHomeVisibilityState.init(mShellTransitions); setStartingWindowListener(mStartingWindowListener); setLauncherUnlockAnimationController( mLauncherActivityClass, mLauncherUnlockAnimationController); @@ -1102,22 +1100,8 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { mRemoteTransitions.remove(remoteTransition); } - public void setHomeTransitionListener(IHomeTransitionListener listener) { - if (!FeatureFlags.enableHomeTransitionListener()) { - return; - } - - mHomeTransitionListener = listener; - - if (mShellTransitions != null) { - try { - mShellTransitions.setHomeTransitionListener(listener); - } catch (RemoteException e) { - Log.w(TAG, "Failed call setHomeTransitionListener", e); - } - } else { - Log.w(TAG, "Unable to call setHomeTransitionListener because ShellTransitions is null"); - } + public HomeVisibilityState getHomeVisibilityState() { + return mHomeVisibilityState; } /** @@ -1558,7 +1542,7 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { pw.println("\tmSplitSelectListener=" + mSplitSelectListener); pw.println("\tmOneHanded=" + mOneHanded); pw.println("\tmShellTransitions=" + mShellTransitions); - pw.println("\tmHomeTransitionListener=" + mHomeTransitionListener); + pw.println("\tmHomeVisibilityState=" + mHomeVisibilityState); pw.println("\tmStartingWindow=" + mStartingWindow); pw.println("\tmStartingWindowListener=" + mStartingWindowListener); pw.println("\tmSysuiUnlockAnimationController=" + mSysuiUnlockAnimationController); From d1bfd5667810a7ef1e837891a702de4dfa4c6aeb Mon Sep 17 00:00:00 2001 From: Holly Sun Date: Mon, 18 Mar 2024 14:58:15 -0700 Subject: [PATCH 07/20] [omni] Read long press duration from AppSearch and override server configured value. Notify the appsearch values to systemui through SystemUiProxy. See http://shortn/_WqYj0buH7R for summary Bug: 330446188 Test: manual. Side load agsa apk to read the value from AppSearch Flag: legacy CUSTOM_LPNH_THRESHOLDS enabled Change-Id: I7bd2688178da48ae8eb9e62e135304cba2fec8ce --- .../src/com/android/quickstep/SystemUiProxy.java | 11 +++++++++++ .../NavHandleLongPressInputConsumer.java | 6 ++++-- .../android/quickstep/util/AssistStateManager.java | 10 ++++++++++ src/com/android/launcher3/config/FeatureFlags.java | 11 ++++++++++- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index b6272dad87..723c65ad1f 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -454,6 +454,17 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { } } + @Override + public void setOverrideHomeButtonLongPress(long duration, float slopMultiplier) { + if (mSystemUiProxy != null) { + try { + mSystemUiProxy.setOverrideHomeButtonLongPress(duration, slopMultiplier); + } catch (RemoteException e) { + Log.w(TAG, "Failed call setOverrideHomeButtonLongPress", e); + } + } + } + @Override public void notifyAccessibilityButtonClicked(int displayId) { if (mSystemUiProxy != null) { diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java index e4a8619338..e22703b659 100644 --- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java +++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java @@ -34,6 +34,7 @@ import com.android.quickstep.InputConsumer; import com.android.quickstep.NavHandle; import com.android.quickstep.RecentsAnimationDeviceState; import com.android.quickstep.TopTaskTracker; +import com.android.quickstep.util.AssistStateManager; import com.android.systemui.shared.system.InputMonitorCompat; /** @@ -64,8 +65,9 @@ public class NavHandleLongPressInputConsumer extends DelegateInputConsumer { super(delegate, inputMonitor); mScreenWidth = DisplayController.INSTANCE.get(context).getInfo().currentSize.x; mDeepPressEnabled = FeatureFlags.ENABLE_LPNH_DEEP_PRESS.get(); - if (FeatureFlags.CUSTOM_LPNH_THRESHOLDS.get()) { - mLongPressTimeout = FeatureFlags.LPNH_TIMEOUT_MS.get(); + AssistStateManager assistStateManager = AssistStateManager.INSTANCE.get(context); + if (assistStateManager.getLPNHDurationMillis().isPresent()) { + mLongPressTimeout = assistStateManager.getLPNHDurationMillis().get().intValue(); } else { mLongPressTimeout = ViewConfiguration.getLongPressTimeout(); } diff --git a/quickstep/src/com/android/quickstep/util/AssistStateManager.java b/quickstep/src/com/android/quickstep/util/AssistStateManager.java index a8546561d2..a1fdbbb127 100644 --- a/quickstep/src/com/android/quickstep/util/AssistStateManager.java +++ b/quickstep/src/com/android/quickstep/util/AssistStateManager.java @@ -52,6 +52,16 @@ public class AssistStateManager implements ResourceBasedOverride { return Optional.empty(); } + /** Get the Launcher overridden long press duration to trigger Assistant. */ + public Optional getLPNHDurationMillis() { + return Optional.empty(); + } + + /** Get the Launcher overridden long press touch slop multiplier to trigger Assistant. */ + public Optional getLPNHCustomSlopMultiplier() { + return Optional.empty(); + } + /** Return {@code true} if the Settings toggle is enabled. */ public boolean isSettingsAllEntrypointsEnabled() { return false; diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index b7c9161d2b..e476138487 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -132,7 +132,16 @@ public final class FeatureFlags { public static final BooleanFlag CUSTOM_LPNH_THRESHOLDS = getReleaseFlag(301680992, "CUSTOM_LPNH_THRESHOLDS", ENABLED, - "Add dev options to customize the LPNH trigger slop and milliseconds"); + "Add dev options and server side control to customize the LPNH " + + "trigger slop and milliseconds"); + + public static final BooleanFlag CUSTOM_LPH_THRESHOLDS = getReleaseFlag(331800576, + "CUSTOM_LPH_THRESHOLDS", DISABLED, + "Server side control to customize LPH timeout and touch slop"); + + public static final BooleanFlag OVERRIDE_LPNH_LPH_THRESHOLDS = getReleaseFlag(331799727, + "OVERRIDE_LPNH_LPH_THRESHOLDS", DISABLED, + "Enable AGSA override for LPNH and LPH timeout and touch slop"); public static final BooleanFlag ANIMATE_LPNH = getReleaseFlag(308693847, "ANIMATE_LPNH", TEAMFOOD, From 2ae49c6cab60613d3c7abd584c4e2f11bc2a2878 Mon Sep 17 00:00:00 2001 From: Ats Jenk Date: Fri, 29 Mar 2024 09:53:59 -0700 Subject: [PATCH 08/20] Update bubble bar location in shell Send wmshell updates about bubble bar position when it is dragged across the center divide. Bug: 330585397 Flag: ACONFIG com.android.wm.shell.enable_bubble_bar DEVELOPMENT Test: manual - with bubble bar on home screen - long press and drag bubble bar around on same side, no drop target - release the bubble bar, it snaps back to the original position - long press and drag bubble bar to other side, see drop target at the corner - release bubble bar, it snaps to the new side - long press and drag bubble bar to other side and back, see the drop target visible, release, snaps back to the original position - repeat above steps from an app, swipe up taskbar for bubble bar Change-Id: I88faf641b9c07a19cfbb7a1feb8170a64269ac1f --- .../taskbar/bubbles/BubbleBarController.java | 26 ++++-- .../taskbar/bubbles/BubbleBarView.java | 88 ++++++++++++++++++- .../taskbar/bubbles/BubbleDragController.java | 31 ++++--- .../com/android/quickstep/SystemUiProxy.java | 13 +++ 4 files changed, 137 insertions(+), 21 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java index 1f3c4839ab..c1c76d0a01 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java @@ -220,7 +220,7 @@ public class BubbleBarController extends IBubblesListener.Stub { mBubbleStashedHandleViewController.setHiddenForBubbles( !sBubbleBarEnabled || mBubbles.isEmpty()); mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse( - key -> setSelectedBubble(mBubbles.get(key))); + key -> setSelectedBubbleInternal(mBubbles.get(key))); }); } @@ -390,7 +390,7 @@ public class BubbleBarController extends IBubblesListener.Stub { } } if (bubbleToSelect != null) { - setSelectedBubble(bubbleToSelect); + setSelectedBubbleInternal(bubbleToSelect); if (previouslySelectedBubble == null) { mBubbleStashController.animateToInitialState(update.expanded); } @@ -409,8 +409,7 @@ public class BubbleBarController extends IBubblesListener.Stub { if (update.bubbleBarLocation != mBubbleBarViewController.getBubbleBarLocation()) { // Animate when receiving updates. Skip it if we received the initial state. boolean animate = !update.initialState; - mBubbleBarViewController.setBubbleBarLocation(update.bubbleBarLocation, animate); - mBubbleStashController.setBubbleBarLocation(update.bubbleBarLocation); + updateBubbleBarLocationInternal(update.bubbleBarLocation, animate); } } } @@ -436,7 +435,7 @@ public class BubbleBarController extends IBubblesListener.Stub { /** Updates the currently selected bubble for launcher views and tells WMShell to show it. */ public void showAndSelectBubble(BubbleBarItem b) { if (DEBUG) Log.w(TAG, "showingSelectedBubble: " + b.getKey()); - setSelectedBubble(b); + setSelectedBubbleInternal(b); showSelectedBubble(); } @@ -445,7 +444,7 @@ public class BubbleBarController extends IBubblesListener.Stub { * WMShell that the selection has changed, that should go through either * {@link #showSelectedBubble()} or {@link #showAndSelectBubble(BubbleBarItem)}. */ - private void setSelectedBubble(BubbleBarItem b) { + private void setSelectedBubbleInternal(BubbleBarItem b) { if (!Objects.equals(b, mSelectedBubble)) { if (DEBUG) Log.w(TAG, "selectingBubble: " + b.getKey()); mSelectedBubble = b; @@ -464,6 +463,21 @@ public class BubbleBarController extends IBubblesListener.Stub { return null; } + /** + * Set a new bubble bar location. + *

+ * Updates the value locally in Launcher and in WMShell. + */ + public void updateBubbleBarLocation(BubbleBarLocation location) { + updateBubbleBarLocationInternal(location, false /* animate */); + mSystemUiProxy.setBubbleBarLocation(location); + } + + private void updateBubbleBarLocationInternal(BubbleBarLocation location, boolean animate) { + mBubbleBarViewController.setBubbleBarLocation(location, animate); + mBubbleStashController.setBubbleBarLocation(location); + } + // // Loading data for the bubbles // diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index 4ca7c89127..c27e9f1287 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -24,7 +24,9 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.PointF; import android.graphics.Rect; import android.util.AttributeSet; import android.util.LayoutDirection; @@ -108,6 +110,7 @@ public class BubbleBarView extends FrameLayout { private final float mIconSize; // The elevation of the bubbles within the bar private final float mBubbleElevation; + private final float mDragElevation; private final int mPointerSize; // Whether the bar is expanded (i.e. the bubble activity is being displayed). @@ -138,11 +141,15 @@ public class BubbleBarView extends FrameLayout { @Nullable private Consumer mUpdateSelectedBubbleAfterCollapse; + private boolean mDragging; + @Nullable private BubbleView mDraggedBubbleView; private int mPreviousLayoutDirection = LayoutDirection.UNDEFINED; + private boolean mLocationChangePending; + public BubbleBarView(Context context) { this(context, null); } @@ -163,6 +170,7 @@ public class BubbleBarView extends FrameLayout { mIconSpacing = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing); mIconSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size); mBubbleElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_elevation); + mDragElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_drag_elevation); mPointerSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_pointer_size); setClipToPadding(false); @@ -237,12 +245,13 @@ public class BubbleBarView extends FrameLayout { } } + @SuppressLint("RtlHardcoded") private void onBubbleBarLocationChanged() { + mLocationChangePending = false; final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl()); mBubbleBarBackground.setAnchorLeft(onLeft); mRelativePivotX = onLeft ? 0f : 1f; - ViewGroup.LayoutParams layoutParams = getLayoutParams(); - if (layoutParams instanceof LayoutParams lp) { + if (getLayoutParams() instanceof LayoutParams lp) { lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT); setLayoutParams(lp); } @@ -267,11 +276,82 @@ public class BubbleBarView extends FrameLayout { } } + /** + * Set whether this view is being currently being dragged + */ + public void setIsDragging(boolean dragging) { + if (mDragging == dragging) { + return; + } + mDragging = dragging; + setElevation(dragging ? mDragElevation : mBubbleElevation); + if (!dragging && mLocationChangePending) { + // During drag finish animation we may update the translation x value to shift the + // bubble to the new drop target. Clear the translation here. + setTranslationX(0f); + onBubbleBarLocationChanged(); + } + } + + /** + * Adjust resting position for the bubble bar while it is being dragged. + *

+ * Bubble bar is laid out on left or right side of the screen. When it is being dragged to + * the opposite side, the resting position should be on that side. Calculate any additional + * translation that may be required to move the bubble bar to the new side. + * + * @param restingPosition relative resting position of the bubble bar from the laid out position + */ + @SuppressLint("RtlHardcoded") + void adjustRelativeRestingPosition(PointF restingPosition) { + final boolean locationOnLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl()); + // Bubble bar is placed left or right with gravity. Check where it is currently. + final int absoluteGravity = Gravity.getAbsoluteGravity( + ((LayoutParams) getLayoutParams()).gravity, getLayoutDirection()); + final boolean gravityOnLeft = + (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT; + + // Bubble bar is pinned to the same side per gravity and the desired location. + // Resting translation does not need to be adjusted. + if (locationOnLeft == gravityOnLeft) { + return; + } + + // Bubble bar is laid out on left or right side of the screen. And the desired new + // location is on the other side. Calculate x translation value required to shift the + // bubble bar from one side to the other. + float x = getDistanceFromOtherSide(); + if (locationOnLeft) { + // New location is on the left, shift left + // before -> |......ooo.| after -> |.ooo......| + restingPosition.x = -x; + } else { + // New location is on the right, shift right + // before -> |.ooo......| after -> |......ooo.| + restingPosition.x = x; + } + } + + private float getDistanceFromOtherSide() { + // Calculate the shift needed to position the bubble bar on the other side + int displayWidth = getResources().getDisplayMetrics().widthPixels; + int margin = 0; + if (getLayoutParams() instanceof MarginLayoutParams lp) { + margin += lp.leftMargin; + margin += lp.rightMargin; + } + return (float) (displayWidth - getWidth() - margin); + } + private void setBubbleBarLocationInternal(BubbleBarLocation bubbleBarLocation) { if (bubbleBarLocation != mBubbleBarLocation) { mBubbleBarLocation = bubbleBarLocation; - onBubbleBarLocationChanged(); - invalidate(); + if (mDragging) { + mLocationChangePending = true; + } else { + onBubbleBarLocationChanged(); + invalidate(); + } } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java index dab7d9d07a..5ffc6d830a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java @@ -25,7 +25,6 @@ import android.view.ViewConfiguration; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.launcher3.R; import com.android.launcher3.taskbar.TaskbarActivityContext; /** @@ -55,9 +54,8 @@ public class BubbleDragController { mBubbleBarViewController = bubbleControllers.bubbleBarViewController; mBubbleDismissController = bubbleControllers.bubbleDismissController; mBubbleBarPinController = bubbleControllers.bubbleBarPinController; - mBubbleBarPinController.setListener(location -> { - // TODO(b/330585397): update bubble bar location in shell - }); + mBubbleBarPinController.setListener( + bubbleControllers.bubbleBarController::updateBubbleBarLocation); mBubbleDismissController.setListener( stuck -> mBubbleBarPinController.setDropTargetHidden(stuck)); } @@ -96,11 +94,8 @@ public class BubbleDragController { @SuppressLint("ClickableViewAccessibility") public void setupBubbleBarView(@NonNull BubbleBarView bubbleBarView) { PointF initialRelativePivot = new PointF(); - final int restingElevation = bubbleBarView.getResources().getDimensionPixelSize( - R.dimen.bubblebar_elevation); - final int dragElevation = bubbleBarView.getResources().getDimensionPixelSize( - R.dimen.bubblebar_drag_elevation); bubbleBarView.setOnTouchListener(new BubbleTouchListener() { + @Override protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) { if (bubbleBarView.isExpanded()) return false; @@ -114,7 +109,7 @@ public class BubbleDragController { // By default the bubble bar view pivot is in bottom right corner, while dragging // it should be centered in order to align it with the dismiss target view bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f); - bubbleBarView.setElevation(dragElevation); + bubbleBarView.setIsDragging(true); mBubbleBarPinController.onDragStart( bubbleBarView.getBubbleBarLocation().isOnLeft(bubbleBarView.isLayoutRtl())); } @@ -138,7 +133,14 @@ public class BubbleDragController { void onDragEnd() { // Restoring the initial pivot for the bubble bar view bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y); - bubbleBarView.setElevation(restingElevation); + bubbleBarView.setIsDragging(false); + } + + @Override + protected PointF getRestingPosition() { + PointF restingPosition = super.getRestingPosition(); + bubbleBarView.adjustRelativeRestingPosition(restingPosition); + return restingPosition; } }); } @@ -226,6 +228,13 @@ public class BubbleDragController { protected void onDragDismiss() { } + /** + * Get the resting position of the view when drag is released + */ + protected PointF getRestingPosition() { + return mViewInitialPosition; + } + @Override @SuppressLint("ClickableViewAccessibility") public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) { @@ -353,7 +362,7 @@ public class BubbleDragController { mAnimator.animateDismiss(mViewInitialPosition, onComplete); } else { onDragRelease(); - mAnimator.animateToInitialState(mViewInitialPosition, getCurrentVelocity(), + mAnimator.animateToInitialState(getRestingPosition(), getCurrentVelocity(), onComplete); } mBubbleDismissController.hideDismissView(); diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index b6272dad87..3c3307ac59 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -82,6 +82,7 @@ import com.android.systemui.unfold.progress.IUnfoldTransitionListener; import com.android.wm.shell.back.IBackAnimation; import com.android.wm.shell.bubbles.IBubbles; import com.android.wm.shell.bubbles.IBubblesListener; +import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.common.pip.IPip; import com.android.wm.shell.common.pip.IPipAnimationListener; import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; @@ -808,6 +809,18 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle { } } + /** + * Tells SysUI to update the bubble bar location to the new location. + * @param location new location for the bubble bar + */ + public void setBubbleBarLocation(BubbleBarLocation location) { + try { + mBubbles.setBubbleBarLocation(location); + } catch (RemoteException e) { + Log.w(TAG, "Failed call setBubbleBarLocation"); + } + } + // // Splitscreen // From 9bdb1daa4dd072aa9fcb6fb5ccc59265f5f7dfc2 Mon Sep 17 00:00:00 2001 From: Peter Kalauskas Date: Tue, 2 Apr 2024 15:38:57 -0700 Subject: [PATCH 09/20] Add soong namespace to sysui libs Test: m checkbuild Flag: NONE Bug: 214238812 Change-Id: I6f0dd217c03da8ad38ee61cf89587dede9660590 --- Android.bp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Android.bp b/Android.bp index 877f7bbbef..c4ea48ad5a 100644 --- a/Android.bp +++ b/Android.bp @@ -75,7 +75,7 @@ android_library { "androidx.test.uiautomator_uiautomator", "androidx.preference_preference", "SystemUISharedLib", - "animationlib", + "//frameworks/libs/systemui:animationlib", "launcher-testing-shared", ], srcs: [ @@ -150,9 +150,9 @@ android_library { "androidx.cardview_cardview", "androidx.window_window", "com.google.android.material_material", - "iconloader_base", - "view_capture", - "animationlib", + "//frameworks/libs/systemui:iconloader_base", + "//frameworks/libs/systemui:view_capture", + "//frameworks/libs/systemui:animationlib", "SystemUI-statsd", "launcher-testing-shared", "androidx.lifecycle_lifecycle-common-java8", From b82a47a0e73958e96de4cc90b88e0c5eb3bb7ab3 Mon Sep 17 00:00:00 2001 From: Johannes Gallmann Date: Wed, 3 Apr 2024 10:40:17 +0200 Subject: [PATCH 10/20] Use custom interpolator for predictive back system animations Bug: 332512902 Flag: ACONFIG com.android.window.flags.predictive_back_system_anims TRUNKFOOD Test: Manual, i.e. analysing screenrecordings and verify that there are no more animation jumps at the start/end of the back animation Change-Id: I588a8a631e068c09e88c1f3c5af5d8970746c803 --- .../com/android/quickstep/LauncherBackAnimationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java index 2d25295402..49f3ba6be5 100644 --- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java +++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java @@ -99,7 +99,7 @@ public class LauncherBackAnimationController { private final int mWindowScaleMarginX; private float mWindowScaleEndCornerRadius; private float mWindowScaleStartCornerRadius; - private final Interpolator mProgressInterpolator = Interpolators.STANDARD_DECELERATE; + private final Interpolator mProgressInterpolator = Interpolators.BACK_GESTURE; private final Interpolator mVerticalMoveInterpolator = new DecelerateInterpolator(); private final PointF mInitialTouchPos = new PointF(); From 06a05c38063c0cb1f4eb70d8e38b1b2b58c63331 Mon Sep 17 00:00:00 2001 From: Jagrut Desai Date: Tue, 2 Apr 2024 13:54:53 -0700 Subject: [PATCH 11/20] Folder Title Change Listener Bug: 305877212 Test: Manual Flag: NONE Change-Id: I10c7e0827a05010720daeb9da7a0be854ee06112 --- src/com/android/launcher3/folder/Folder.java | 5 +++++ src/com/android/launcher3/folder/FolderIcon.java | 1 + src/com/android/launcher3/model/data/FolderInfo.java | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index c8c634a741..df01a65529 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -1432,6 +1432,11 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo updateTextViewFocus(); } + @Override + public void onTitleChanged(CharSequence title) { + mFolderName.setText(title); + } + /** * Utility methods to iterate over items of the view */ diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java index ee0d5fce24..cf984bb19f 100644 --- a/src/com/android/launcher3/folder/FolderIcon.java +++ b/src/com/android/launcher3/folder/FolderIcon.java @@ -718,6 +718,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel requestLayout(); } + @Override public void onTitleChanged(CharSequence title) { mFolderName.setText(title); setContentDescription(getAccessiblityTitle(title)); diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java index 83ba2b3a9f..4a1d7447ae 100644 --- a/src/com/android/launcher3/model/data/FolderInfo.java +++ b/src/com/android/launcher3/model/data/FolderInfo.java @@ -189,6 +189,8 @@ public class FolderInfo extends ItemInfo { void onAdd(WorkspaceItemInfo item, int rank); void onRemove(List item); void onItemsChanged(boolean animate); + void onTitleChanged(CharSequence title); + } public boolean hasOption(int optionFlag) { @@ -261,6 +263,10 @@ public class FolderInfo extends ItemInfo { if (modelWriter != null) { modelWriter.updateItemInDatabase(this); } + + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).onTitleChanged(title); + } } /** From 2cf24c84bff48cd962de43100c956ac379ced3ec Mon Sep 17 00:00:00 2001 From: Himanshu Gupta Date: Wed, 3 Apr 2024 18:49:08 +0100 Subject: [PATCH 12/20] Fix failing test. Moving to All Apps view is not required as we are already on the same view during `moveToSearchView` invocation. Bug: 329152799 Test: LauncherIntentTest#testAllAppsIntent Flag: NA Change-Id: Ia618eaa1999a9db663dd09bb7c150b5a51dedc36 --- tests/src/com/android/launcher3/LauncherIntentTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/src/com/android/launcher3/LauncherIntentTest.java b/tests/src/com/android/launcher3/LauncherIntentTest.java index e8822c3712..a3013c72c6 100644 --- a/tests/src/com/android/launcher3/LauncherIntentTest.java +++ b/tests/src/com/android/launcher3/LauncherIntentTest.java @@ -29,7 +29,6 @@ import com.android.launcher3.allapps.ActivityAllAppsContainerView; import com.android.launcher3.allapps.SearchRecyclerView; import com.android.launcher3.ui.AbstractLauncherUiTest; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,7 +39,6 @@ public class LauncherIntentTest extends AbstractLauncherUiTest { public final Intent allAppsIntent = new Intent(Intent.ACTION_ALL_APPS); @Test - @Ignore("b/329152799") public void testAllAppsIntent() { // setup by moving to home mLauncher.goHome(); @@ -66,8 +64,6 @@ public class LauncherIntentTest extends AbstractLauncherUiTest { // Highlights the search bar, then fills text to display the SearchView. private void moveToSearchView() { - mLauncher.goHome().switchToAllApps(); - // All Apps view should be loaded assertTrue("Launcher internal state is not All Apps", isInState(() -> LauncherState.ALL_APPS)); From 4d341404c6cadc0d2cadb58d536c19c233d19086 Mon Sep 17 00:00:00 2001 From: Zak Cohen Date: Tue, 2 Apr 2024 15:17:17 -0700 Subject: [PATCH 13/20] Align widget picker bottom swipe transition with predictive back. Bug: 325930715 Test: widget picker tests Flag: NA Change-Id: I15319f0a264503ff34dd4cc0dc36a40531379e2b --- .../allapps/TaskbarAllAppsSlideInView.java | 10 ++--- .../launcher3/views/AbstractSlideInView.java | 40 ++++++++++--------- .../launcher3/views/WidgetsEduView.java | 6 +-- .../launcher3/widget/WidgetsBottomSheet.java | 2 +- .../widget/picker/WidgetsFullSheet.java | 6 +-- .../widget/picker/WidgetsTwoPaneSheet.java | 14 +++---- 6 files changed, 40 insertions(+), 38 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java index 3b04602811..99937f841b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java +++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java @@ -207,10 +207,10 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView }; protected static final float TRANSLATION_SHIFT_CLOSED = 1f; protected static final float TRANSLATION_SHIFT_OPENED = 0f; - private static final float VIEW_NO_SCALE = 1f; private static final int DEFAULT_DURATION = 300; protected final T mActivityContext; @@ -129,9 +128,13 @@ public abstract class AbstractSlideInView protected @Nullable OnCloseListener mOnCloseBeginListener; protected List mOnCloseListeners = new ArrayList<>(); - protected final AnimatedFloat mSlideInViewScale = - new AnimatedFloat(this::onScaleProgressChanged, VIEW_NO_SCALE); - protected boolean mIsBackProgressing; + /** + * How far through a "user initiated dismissal" the UI is. e.g. Predictive back, swipe to home, + * 0 is regular state, 1 is fully dismissed. + */ + protected final AnimatedFloat mSwipeToDismissProgress = + new AnimatedFloat(this::onUserSwipeToDismissProgressChanged, 0f); + protected boolean mIsDismissInProgress; private @Nullable Drawable mContentBackground; private @Nullable View mContentBackgroundParentView; @@ -287,29 +290,30 @@ public abstract class AbstractSlideInView final float progress = backEvent.getProgress(); float deceleratedProgress = Interpolators.PREDICTIVE_BACK_DECELERATED_EASE.getInterpolation(progress); - mIsBackProgressing = progress > 0f; - mSlideInViewScale.updateValue(PREDICTIVE_BACK_MIN_SCALE - + (1 - PREDICTIVE_BACK_MIN_SCALE) * (1 - deceleratedProgress)); + mSwipeToDismissProgress.updateValue(deceleratedProgress); } - protected void onScaleProgressChanged() { - float scaleProgress = mSlideInViewScale.value; - SCALE_PROPERTY.set(this, scaleProgress); - setClipChildren(!mIsBackProgressing); - setClipToPadding(!mIsBackProgressing); - mContent.setClipChildren(!mIsBackProgressing); - mContent.setClipToPadding(!mIsBackProgressing); + protected void onUserSwipeToDismissProgressChanged() { + float progress = mSwipeToDismissProgress.value; + mIsDismissInProgress = progress > 0f; + + float scale = PREDICTIVE_BACK_MIN_SCALE + (1 - PREDICTIVE_BACK_MIN_SCALE) * (1f - progress); + SCALE_PROPERTY.set(this, scale); + setClipChildren(!mIsDismissInProgress); + setClipToPadding(!mIsDismissInProgress); + mContent.setClipChildren(!mIsDismissInProgress); + mContent.setClipToPadding(!mIsDismissInProgress); invalidate(); } @Override public void onBackCancelled() { super.onBackCancelled(); - animateSlideInViewToNoScale(); + animateSwipeToDismissProgressToStart(); } - protected void animateSlideInViewToNoScale() { - mSlideInViewScale.animateToValue(1f) + protected void animateSwipeToDismissProgressToStart() { + mSwipeToDismissProgress.animateToValue(0f) .setDuration(REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS) .start(); } @@ -340,7 +344,7 @@ public abstract class AbstractSlideInView mContentBackgroundParentView.getTop() + (int) mContent.getTranslationY(), mContentBackgroundParentView.getRight(), mContentBackgroundParentView.getBottom() - + (mIsBackProgressing ? getBottomOffsetPx() : 0)); + + (mIsDismissInProgress ? getBottomOffsetPx() : 0)); mContentBackground.draw(canvas); } diff --git a/src/com/android/launcher3/views/WidgetsEduView.java b/src/com/android/launcher3/views/WidgetsEduView.java index 45ff9de503..53fbd8f33b 100644 --- a/src/com/android/launcher3/views/WidgetsEduView.java +++ b/src/com/android/launcher3/views/WidgetsEduView.java @@ -70,9 +70,9 @@ public class WidgetsEduView extends AbstractSlideInView implements } @Override - protected void onScaleProgressChanged() { - super.onScaleProgressChanged(); - setTranslationY(getMeasuredHeight() * (1 - mSlideInViewScale.value) / 2); + protected void onUserSwipeToDismissProgressChanged() { + super.onUserSwipeToDismissProgressChanged(); + setTranslationY(getMeasuredHeight() * (mSwipeToDismissProgress.value / 2)); } private void show() { diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java index e6b9c9b7f9..f1b80e41d1 100644 --- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java @@ -280,6 +280,6 @@ public class WidgetsBottomSheet extends BaseWidgetSheet { @Override public void addHintCloseAnim( float distanceToMove, Interpolator interpolator, PendingAnimation target) { - target.setInt(this, PADDING_BOTTOM, (int) (distanceToMove + mInsets.bottom), interpolator); + target.addAnimatedFloat(mSwipeToDismissProgress, 0f, 1f, interpolator); } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 5cf8203b84..85375eec9d 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -17,7 +17,6 @@ package com.android.launcher3.widget.picker; import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker; -import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.LauncherPrefs.WIDGETS_EDUCATION_DIALOG_SEEN; import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.SEARCH; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED; @@ -814,8 +813,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet @Override public void addHintCloseAnim( float distanceToMove, Interpolator interpolator, PendingAnimation target) { - target.setFloat(getRecyclerView(), VIEW_TRANSLATE_Y, -distanceToMove, interpolator); - target.setViewAlpha(getRecyclerView(), 0.5f, interpolator); + target.addAnimatedFloat(mSwipeToDismissProgress, 0f, 1f, interpolator); } @Override @@ -911,7 +909,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet public void onBackInvoked() { if (mIsInSearchMode) { mSearchBar.reset(); - animateSlideInViewToNoScale(); + animateSwipeToDismissProgressToStart(); } else { super.onBackInvoked(); } diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java index c60bca0b29..1bf813c89c 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java @@ -74,7 +74,7 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { private ScrollView mRightPaneScrollView; private WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder; - private boolean mOldIsBackSwipeProgressing; + private boolean mOldIsSwipeToDismissInProgress; private int mActivePage = -1; private PackageUserKey mSelectedHeader; @@ -154,14 +154,14 @@ public class WidgetsTwoPaneSheet extends WidgetsFullSheet { } @Override - protected void onScaleProgressChanged() { - super.onScaleProgressChanged(); - boolean isBackSwipeProgressing = mSlideInViewScale.value > 0; - if (isBackSwipeProgressing == mOldIsBackSwipeProgressing) { + protected void onUserSwipeToDismissProgressChanged() { + super.onUserSwipeToDismissProgressChanged(); + boolean isSwipeToDismissInProgress = mSwipeToDismissProgress.value > 0; + if (isSwipeToDismissInProgress == mOldIsSwipeToDismissInProgress) { return; } - mOldIsBackSwipeProgressing = isBackSwipeProgressing; - if (isBackSwipeProgressing) { + mOldIsSwipeToDismissInProgress = isSwipeToDismissInProgress; + if (isSwipeToDismissInProgress) { modifyAttributesOnViewTree(mPrimaryWidgetListView, (ViewParent) mContent, CLIP_CHILDREN_FALSE_MODIFIER); modifyAttributesOnViewTree(mRightPaneScrollView, (ViewParent) mContent, From 43b731801be3af54d234b490ee86a004ea441be1 Mon Sep 17 00:00:00 2001 From: Eghosa Ewansiha-Vlachavas Date: Wed, 3 Apr 2024 11:16:43 +0000 Subject: [PATCH 14/20] Fix launcher activity leaking when desktop windowing enabled When desktop windowing is enabled we initiate the `SplitFromDesktopController` within the `SplitSelectStateController` which registers a `SplitSelectListener` on the launcher instance. However when we destroy the `SplitSelectStateController` the `SplitFromDesktopController` is not destroyed an thus any listeners are not unregisterd. Instead we should explicitly destroy the `SplitFromDesktopController` by unregistering listener. Flag: NONE Fixes: 332667403 Bug: 331774319 Test: atest -c NexusLauncherTests:com.android.quickstep.TaplStartLauncherViaGestureTests atest -c NexusLauncherTests:com.android.quickstep.util.SplitSelectStateControllerTest Change-Id: I68ffcc4114644e75f751632eca8bc73e406139a8 --- .../util/SplitSelectStateController.java | 17 ++++++++++++++++- .../util/SplitSelectStateControllerTest.kt | 14 ++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java index d8cbbf9d10..408949850a 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java @@ -70,6 +70,7 @@ import android.window.RemoteTransition; import android.window.TransitionInfo; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.launcher3.Launcher; @@ -132,6 +133,7 @@ public class SplitSelectStateController { private final StatsLogManager mStatsLogManager; private final SystemUiProxy mSystemUiProxy; private final StateManager mStateManager; + @Nullable private SplitFromDesktopController mSplitFromDesktopController; @Nullable private DepthController mDepthController; @@ -208,6 +210,9 @@ public class SplitSelectStateController { mActivityBackCallback = null; mAppPairsController.onDestroy(); mSplitSelectDataHolder.onDestroy(); + if (mSplitFromDesktopController != null) { + mSplitFromDesktopController.onDestroy(); + } } /** @@ -643,7 +648,12 @@ public class SplitSelectStateController { } public void initSplitFromDesktopController(Launcher launcher) { - mSplitFromDesktopController = new SplitFromDesktopController(launcher); + initSplitFromDesktopController(new SplitFromDesktopController(launcher)); + } + + @VisibleForTesting + void initSplitFromDesktopController(SplitFromDesktopController controller) { + mSplitFromDesktopController = controller; } private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId, @@ -977,6 +987,11 @@ public class SplitSelectStateController { SystemUiProxy.INSTANCE.get(mLauncher).registerSplitSelectListener(mSplitSelectListener); } + void onDestroy() { + SystemUiProxy.INSTANCE.get(mLauncher).unregisterSplitSelectListener( + mSplitSelectListener); + } + /** * Enter split select from desktop mode. * @param taskInfo the desktop task to move to split stage diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt index 18b1ea0260..a7ed8a73b2 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt @@ -36,6 +36,7 @@ import com.android.launcher3.util.ComponentKey import com.android.launcher3.util.SplitConfigurationOptions import com.android.quickstep.RecentsModel import com.android.quickstep.SystemUiProxy +import com.android.quickstep.util.SplitSelectStateController.SplitFromDesktopController import com.android.systemui.shared.recents.model.Task import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50 import java.util.function.Consumer @@ -65,6 +66,7 @@ class SplitSelectStateControllerTest { private val context: StatefulActivity<*> = mock() private val recentsModel: RecentsModel = mock() private val pendingIntent: PendingIntent = mock() + private val splitFromDesktopController: SplitFromDesktopController = mock() private lateinit var splitSelectStateController: SplitSelectStateController @@ -607,6 +609,18 @@ class SplitSelectStateControllerTest { assertTrue(splitSelectStateController.isBothSplitAppsConfirmed) } + @Test + fun splitSelectStateControllerDestroyed_SplitFromDesktopControllerAlsoDestroyed() { + // Initiate split from desktop controller + splitSelectStateController.initSplitFromDesktopController(splitFromDesktopController) + + // Simulate default controller being destroyed + splitSelectStateController.onDestroy() + + // Verify desktop controller is also destroyed + verify(splitFromDesktopController).onDestroy() + } + // Generate GroupTask with default userId. private fun generateGroupTask( task1ComponentName: ComponentName, From 0e321148472f92c4b090a79a65b0b78ad0de2dcb Mon Sep 17 00:00:00 2001 From: Jagrut Desai Date: Wed, 3 Apr 2024 16:44:33 -0700 Subject: [PATCH 15/20] Downgrade Test to PostSubmit Test: Presubmit Bug: 332663601 Bug: 332532966 Flag: NONE Change-Id: I1baf598210e4f326ac1fb53662a10a4f4359a5e5 --- .../src/com/android/quickstep/TaplTestsPersistentTaskbar.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java index df73e0913e..a9ff161cbc 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsPersistentTaskbar.java @@ -15,6 +15,8 @@ */ package com.android.quickstep; +import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; +import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT; import android.graphics.Rect; @@ -23,6 +25,7 @@ import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape; +import com.android.launcher3.util.rule.TestStabilityRule; import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch; import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch; @@ -45,6 +48,7 @@ public class TaplTestsPersistentTaskbar extends AbstractTaplTestsTaskbar { @Test @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON) + @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) public void testThreeButtonsTaskbarBoundsAfterConfigChangeDuringIme() { Rect taskbarBoundsBefore = getTaskbar().getVisibleBounds(); // Go home and to an IME activity (any configuration change would do, as long as it From a596f589c4de70050cc0343a037e06f6860e2bdc Mon Sep 17 00:00:00 2001 From: Jeremy Sim Date: Tue, 2 Apr 2024 01:21:31 -0700 Subject: [PATCH 16/20] Allow app pairs in folders This CL substantially refactors folders to be able to take contents of type AppPairInfo. App pairs can now be moved in and out of folders, and launch from folders. This CL contains only logic and model changes; animation and style changes (for dropping items into folders, color changes to app pair surfaces, etc.) will be in a following CL. Another CL (hopefully) will contain tests. I'm planning to submit them together, but this patch should also be able to stand alone with no issues (except janky transitions). Bug: 315731527 Flag: ACONFIG com.android.wm.shell.enable_app_pairs TRUNKFOOD Test: Manual, more to follow in another CL. Change-Id: I73732fcaefbdc61bf6e02a5be365962b8bbc3e41 --- .../taskbar/TaskbarPopupController.java | 2 +- .../quickstep/util/AppPairsController.java | 2 +- .../util/SplitAnimationController.kt | 1 + res/layout/folder_app_pair.xml | 39 ++++++++++ src/com/android/launcher3/Workspace.java | 23 +++--- .../WorkspaceAccessibilityHelper.java | 7 +- .../launcher3/apppairs/AppPairIcon.java | 14 +++- .../apppairs/AppPairIconDrawable.java | 13 +++- .../apppairs/AppPairIconDrawingParams.kt | 31 +++++--- .../launcher3/apppairs/AppPairIconGraphic.kt | 20 ++--- src/com/android/launcher3/folder/Folder.java | 77 +++++++++++-------- .../folder/FolderAnimationManager.java | 74 ++++++++++-------- .../android/launcher3/folder/FolderIcon.java | 47 ++++++----- .../launcher3/folder/FolderPagedView.java | 60 ++++++++++----- .../launcher3/folder/LauncherDelegate.java | 4 +- .../folder/PreviewItemDrawingParams.java | 4 +- .../launcher3/folder/PreviewItemManager.java | 59 +++++++++----- .../android/launcher3/model/BgDataModel.java | 12 ++- .../launcher3/model/FirstScreenBroadcast.java | 2 +- .../android/launcher3/model/LoaderTask.java | 17 ++-- .../launcher3/model/WorkspaceItemProcessor.kt | 2 +- .../launcher3/model/data/AppPairInfo.kt | 35 ++++++++- .../launcher3/model/data/CollectionInfo.kt | 22 +++--- .../launcher3/model/data/FolderInfo.java | 66 +++++++++++++--- .../folder/PreviewItemManagerTest.kt | 28 +++---- .../model/CacheDataUpdatedTaskTest.java | 2 +- .../launcher3/model/FolderIconLoadTest.kt | 4 +- .../launcher3/util/ItemInflaterTest.kt | 20 ++--- 28 files changed, 454 insertions(+), 233 deletions(-) create mode 100644 res/layout/folder_app_pair.xml diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java index 4462f20810..2730be1b57 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java @@ -118,7 +118,7 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba FolderInfo fi = (FolderInfo) info; if (fi.anyMatch(matcher)) { FolderDotInfo folderDotInfo = new FolderDotInfo(); - for (WorkspaceItemInfo si : fi.getContents()) { + for (ItemInfo si : fi.getContents()) { folderDotInfo.addDotInfo(mPopupDataProvider.getDotInfoForItem(si)); } ((FolderIcon) v).setDotInfo(folderDotInfo); diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java index f4da867646..59bf10533c 100644 --- a/quickstep/src/com/android/quickstep/util/AppPairsController.java +++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java @@ -153,7 +153,7 @@ public class AppPairsController { IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache(); MODEL_EXECUTOR.execute(() -> { - newAppPair.getContents().forEach(member -> { + newAppPair.getAppContents().forEach(member -> { member.title = ""; member.bitmap = iconCache.getDefaultIcon(newAppPair.user); iconCache.getTitleAndIcon(member, member.usingLowResIcon()); diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt index 6b27004593..a2d3859135 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -671,6 +671,7 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC appIcon2, dividerPos ) + floatingView.bringToFront() // Launcher animation: animate the floating view, expanding to fill the display surface progressUpdater.addUpdateListener( diff --git a/res/layout/folder_app_pair.xml b/res/layout/folder_app_pair.xml new file mode 100644 index 0000000000..acecd46314 --- /dev/null +++ b/res/layout/folder_app_pair.xml @@ -0,0 +1,39 @@ + + + + + + \ No newline at end of file diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index ce3c55adb7..e80156cd1f 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -17,7 +17,7 @@ package com.android.launcher3; import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY; -import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; +import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.EDIT_MODE; import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE; @@ -1873,12 +1873,9 @@ public class Workspace extends PagedView return false; } - boolean aboveShortcut = (dropOverView.getTag() instanceof WorkspaceItemInfo - && ((WorkspaceItemInfo) dropOverView.getTag()).container - != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION); - boolean willBecomeShortcut = - (info.itemType == ITEM_TYPE_APPLICATION || - info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT); + boolean aboveShortcut = Folder.willAccept(dropOverView.getTag()) + && ((ItemInfo) dropOverView.getTag()).container != CONTAINER_HOTSEAT_PREDICTION; + boolean willBecomeShortcut = Folder.willAcceptItemType(info.itemType); return (aboveShortcut && willBecomeShortcut); } @@ -1925,12 +1922,12 @@ public class Workspace extends PagedView mCreateUserFolderOnDrop = false; final int screenId = getCellLayoutId(target); - boolean aboveShortcut = (v.getTag() instanceof WorkspaceItemInfo); - boolean willBecomeShortcut = (newView.getTag() instanceof WorkspaceItemInfo); + boolean aboveShortcut = Folder.willAccept(v.getTag()); + boolean willBecomeShortcut = Folder.willAccept(newView.getTag()); if (aboveShortcut && willBecomeShortcut) { - WorkspaceItemInfo sourceInfo = (WorkspaceItemInfo) newView.getTag(); - WorkspaceItemInfo destInfo = (WorkspaceItemInfo) v.getTag(); + ItemInfo sourceInfo = (ItemInfo) newView.getTag(); + ItemInfo destInfo = (ItemInfo) v.getTag(); // if the drag started here, we need to remove it from the workspace if (!external) { getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell); @@ -3314,7 +3311,7 @@ public class Workspace extends PagedView } } else if (child instanceof FolderIcon) { FolderInfo folderInfo = (FolderInfo) info; - List matches = folderInfo.getContents().stream() + List matches = folderInfo.getContents().stream() .filter(matcher) .collect(Collectors.toList()); if (!matches.isEmpty()) { @@ -3381,7 +3378,7 @@ public class Workspace extends PagedView FolderInfo fi = (FolderInfo) info; if (fi.anyMatch(matcher)) { FolderDotInfo folderDotInfo = new FolderDotInfo(); - for (WorkspaceItemInfo si : fi.getContents()) { + for (ItemInfo si : fi.getContents()) { folderDotInfo.addDotInfo(mLauncher.getDotInfoForItem(si)); } ((FolderIcon) v).setDotInfo(folderDotInfo); diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java index 52073cc77a..84d6a6fea5 100644 --- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java +++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java @@ -23,6 +23,7 @@ import android.view.View; import com.android.launcher3.CellLayout; import com.android.launcher3.R; import com.android.launcher3.accessibility.BaseAccessibilityDelegate.DragType; +import com.android.launcher3.folder.Folder; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; @@ -117,7 +118,7 @@ public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelega return mContext.getString(R.string.item_moved); } else { ItemInfo info = (ItemInfo) child.getTag(); - if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) { + if (Folder.willAccept(info)) { return mContext.getString(R.string.folder_created); } else if (info instanceof FolderInfo) { @@ -148,8 +149,8 @@ public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelega if (TextUtils.isEmpty(info.title)) { // Find the first item in the folder. FolderInfo folder = (FolderInfo) info; - WorkspaceItemInfo firstItem = null; - for (WorkspaceItemInfo shortcut : folder.getContents()) { + ItemInfo firstItem = null; + for (ItemInfo shortcut : folder.getContents()) { if (firstItem == null || firstItem.rank > shortcut.rank) { firstItem = shortcut; } diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java index 9010f8215d..8ce1c19c47 100644 --- a/src/com/android/launcher3/apppairs/AppPairIcon.java +++ b/src/com/android/launcher3/apppairs/AppPairIcon.java @@ -30,11 +30,13 @@ import androidx.annotation.Nullable; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Reorderable; import com.android.launcher3.dragndrop.DraggableView; +import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.data.AppPairInfo; -import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.views.ActivityContext; @@ -179,10 +181,18 @@ public class AppPairIcon extends FrameLayout implements DraggableView, Reorderab return mIconGraphic; } + /** + * Ensures that both app icons in the pair are loaded in high resolution. + */ + public void verifyHighRes() { + IconCache iconCache = LauncherAppState.getInstance(getContext()).getIconCache(); + getInfo().fetchHiResIconsIfNeeded(iconCache); + } + /** * Called when WorkspaceItemInfos get updated, and the app pair icon may need to be redrawn. */ - public void maybeRedrawForWorkspaceUpdate(Predicate itemCheck) { + public void maybeRedrawForWorkspaceUpdate(Predicate itemCheck) { // If either of the app pair icons return true on the predicate (i.e. in the list of // updated apps), redraw the icon graphic (icon background and both icons). if (getInfo().anyMatch(itemCheck)) { diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java index c0ac11a4a1..db83d91a4b 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java +++ b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java @@ -32,7 +32,7 @@ import com.android.launcher3.icons.FastBitmapDrawable; * A composed Drawable consisting of the two app pair icons and the background behind them (looks * like two rectangles). */ -class AppPairIconDrawable extends Drawable { +public class AppPairIconDrawable extends Drawable { private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final AppPairIconDrawingParams mP; private final FastBitmapDrawable mIcon1; @@ -102,6 +102,7 @@ class AppPairIconDrawable extends Drawable { } mIcon2.draw(canvas); + canvas.restore(); } /** @@ -205,4 +206,14 @@ class AppPairIconDrawable extends Drawable { public void setColorFilter(ColorFilter colorFilter) { mBackgroundPaint.setColorFilter(colorFilter); } + + @Override + public int getIntrinsicWidth() { + return mP.getIconSize(); + } + + @Override + public int getIntrinsicHeight() { + return mP.getIconSize(); + } } diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt index 62e5771776..e2c267044a 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt +++ b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt @@ -77,22 +77,29 @@ class AppPairIconDrawingParams(val context: Context, container: Int) { innerPadding = iconSize * INNER_PADDING_SCALE memberIconSize = iconSize * MEMBER_ICON_SCALE updateOrientation(dp) - if (container == DISPLAY_FOLDER) { - val ta = - context.theme.obtainStyledAttributes( - intArrayOf(R.attr.materialColorSurfaceContainerLowest) - ) - bgColor = ta.getColor(0, 0) - ta.recycle() - } else { - val ta = context.theme.obtainStyledAttributes(R.styleable.FolderIconPreview) - bgColor = ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0) - ta.recycle() - } + bgColor = getBgColorForContainer(container) } /** Checks the device orientation and updates isLeftRightSplit accordingly. */ fun updateOrientation(dp: DeviceProfile) { isLeftRightSplit = dp.isLeftRightSplit } + + private fun getBgColorForContainer(container: Int): Int { + val color: Int + if (container == DISPLAY_FOLDER) { + val ta = + context.theme.obtainStyledAttributes( + intArrayOf(R.attr.materialColorSurfaceContainerLowest) + ) + color = ta.getColor(0, 0) + ta.recycle() + } else { + val ta = context.theme.obtainStyledAttributes(R.styleable.FolderIconPreview) + color = ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0) + ta.recycle() + } + + return color + } } diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt index a3a1cfccde..86ee0c00ba 100644 --- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt +++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt @@ -19,7 +19,6 @@ package com.android.launcher3.apppairs import android.content.Context import android.graphics.Canvas import android.graphics.Rect -import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.Gravity import android.widget.FrameLayout @@ -52,7 +51,10 @@ constructor(context: Context, attrs: AttributeSet? = null) : * 2) One of the member apps can't be launched due to screen size requirements. */ @JvmStatic - fun composeDrawable(appPairInfo: AppPairInfo, p: AppPairIconDrawingParams): Drawable { + fun composeDrawable( + appPairInfo: AppPairInfo, + p: AppPairIconDrawingParams + ): AppPairIconDrawable { // Generate new icons, using themed flag if needed. val flags = if (Themes.isThemedIconEnabled(p.context)) BitmapInfo.FLAG_THEMED else 0 val appIcon1 = appPairInfo.getFirstApp().newIcon(p.context, flags) @@ -81,7 +83,7 @@ constructor(context: Context, attrs: AttributeSet? = null) : private lateinit var parentIcon: AppPairIcon private lateinit var drawParams: AppPairIconDrawingParams - private lateinit var drawable: Drawable + lateinit var drawable: AppPairIconDrawable fun init(icon: AppPairIcon, container: Int) { parentIcon = icon @@ -116,12 +118,6 @@ constructor(context: Context, attrs: AttributeSet? = null) : redraw() } - /** Updates the icon drawable and redraws it */ - fun redraw() { - drawable = composeDrawable(parentIcon.info, drawParams) - invalidate() - } - /** * Gets this icon graphic's visual bounds, with respect to the parent icon's coordinate system. */ @@ -136,6 +132,12 @@ constructor(context: Context, attrs: AttributeSet? = null) : ) } + /** Updates the icon drawable and redraws it */ + fun redraw() { + drawable = composeDrawable(parentIcon.info, drawParams) + invalidate() + } + override fun dispatchDraw(canvas: Canvas) { super.dispatchDraw(canvas) drawable.draw(canvas) diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index aa3c5ba824..f4afb4a136 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -19,6 +19,9 @@ package com.android.launcher3.folder; import static android.text.TextUtils.isEmpty; import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; +import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; import static com.android.launcher3.LauncherState.EDIT_MODE; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent; @@ -66,14 +69,12 @@ import androidx.core.content.res.ResourcesCompat; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Alarm; -import com.android.launcher3.BubbleTextView; import com.android.launcher3.CellLayout; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget; import com.android.launcher3.ExtendedEditText; import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherSettings; import com.android.launcher3.OnAlarmListener; import com.android.launcher3.R; import com.android.launcher3.ShortcutAndWidgetContainer; @@ -166,6 +167,22 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo private static final Rect sTempRect = new Rect(); private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10; + /** + * Checks if {@code o} is an {@link ItemInfo} type that can be placed in folders. + */ + public static boolean willAccept(Object o) { + return o instanceof ItemInfo info && willAcceptItemType(info.itemType); + } + + /** + * Checks if {@code itemType} is a type that can be placed in folders. + */ + public static boolean willAcceptItemType(int itemType) { + return itemType == ITEM_TYPE_APPLICATION + || itemType == ITEM_TYPE_DEEP_SHORTCUT + || itemType == ITEM_TYPE_APP_PAIR; + } + private final Alarm mReorderAlarm = new Alarm(Looper.getMainLooper()); private final Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper()); private final Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper()); @@ -313,9 +330,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo public boolean startDrag(View v, DragOptions options) { Object tag = v.getTag(); - if (tag instanceof WorkspaceItemInfo) { - WorkspaceItemInfo item = (WorkspaceItemInfo) tag; - + if (tag instanceof ItemInfo item) { mEmptyCellRank = item.rank; mCurrentDragView = v; @@ -346,14 +361,12 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } mContent.removeItem(mCurrentDragView); - if (dragObject.dragInfo instanceof WorkspaceItemInfo) { - mItemsInvalidated = true; + mItemsInvalidated = true; - // We do not want to get events for the item being removed, as they will get handled - // when the drop completes - try (SuppressInfoChanges s = new SuppressInfoChanges()) { - mInfo.remove((WorkspaceItemInfo) dragObject.dragInfo, true); - } + // We do not want to get events for the item being removed, as they will get handled + // when the drop completes + try (SuppressInfoChanges s = new SuppressInfoChanges()) { + mInfo.remove(dragObject.dragInfo, true); } mDragInProgress = true; mItemAddedBackToSelfViaIcon = false; @@ -493,7 +506,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo mInfo = info; mFromTitle = info.title; mFromLabelState = info.getFromLabelState(); - ArrayList children = info.getContents(); + ArrayList children = info.getContents(); Collections.sort(children, ITEM_POS_COMPARATOR); updateItemLocationsInDatabaseBatch(true); @@ -626,7 +639,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo // onDropComplete. Perform cleanup once drag-n-drop ends. mDragController.addDragListener(this); - ArrayList items = new ArrayList<>(mInfo.getContents()); + ArrayList items = new ArrayList<>(mInfo.getContents()); mEmptyCellRank = items.size(); items.add(null); // Add an empty spot at the end @@ -647,7 +660,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo * is animated relative to the specified View. If the View is null, no animation * is played. */ - private void animateOpen(List items, int pageNo) { + private void animateOpen(List items, int pageNo) { if (items == null || items.size() <= 1) { Log.d(TAG, "Couldn't animate folder open because items is: " + items); return; @@ -896,8 +909,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo public boolean acceptDrop(DragObject d) { final ItemInfo item = d.dragInfo; final int itemType = item.itemType; - return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || - itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)); + return Folder.willAcceptItemType(itemType); } public void onDragEnter(DragObject d) { @@ -1050,7 +1062,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } } else { // The drag failed, we need to return the item to the folder - WorkspaceItemInfo info = (WorkspaceItemInfo) d.dragInfo; + ItemInfo info = d.dragInfo; View icon = (mCurrentDragView != null && mCurrentDragView.getTag() == info) ? mCurrentDragView : mContent.createNewView(info); ArrayList views = getIconsInReadingOrder(); @@ -1099,7 +1111,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo ArrayList items = new ArrayList<>(); int total = mInfo.getContents().size(); for (int i = 0; i < total; i++) { - WorkspaceItemInfo itemInfo = mInfo.getContents().get(i); + ItemInfo itemInfo = mInfo.getContents().get(i); if (verifier.updateRankAndPos(itemInfo, i)) { items.add(itemInfo); } @@ -1112,8 +1124,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo Executors.MODEL_EXECUTOR.post(() -> { FolderNameInfos nameInfos = new FolderNameInfos(); FolderNameProvider fnp = FolderNameProvider.newInstance(getContext()); - fnp.getSuggestedFolderName( - getContext(), mInfo.getContents(), nameInfos); + fnp.getSuggestedFolderName(getContext(), mInfo.getAppContents(), nameInfos); mInfo.suggestedFolderNames = nameInfos; }); } @@ -1298,15 +1309,15 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo d.deferDragViewCleanupPostAnimation = false; mRearrangeOnClose = true; } else { - final WorkspaceItemInfo si; + final ItemInfo si; if (pasiSi != null) { si = pasiSi; } else if (d.dragInfo instanceof WorkspaceItemFactory) { // Came from all apps -- make a copy. si = ((WorkspaceItemFactory) d.dragInfo).makeWorkspaceItem(launcher); } else { - // WorkspaceItemInfo - si = (WorkspaceItemInfo) d.dragInfo; + // WorkspaceItemInfo or AppPairInfo + si = d.dragInfo; } View currentDragView; @@ -1314,7 +1325,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank); // Actually move the item in the database if it was an external drag. Call this - // before creating the view, so that WorkspaceItemInfo is updated appropriately. + // before creating the view, so that the ItemInfo is updated appropriately. mLauncherDelegate.getModelWriter().addOrMoveItemInDatabase( si, mInfo.id, 0, si.cellX, si.cellY); mIsExternalDrag = false; @@ -1376,14 +1387,14 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo // This is used so the item doesn't immediately appear in the folder when added. In one case // we need to create the illusion that the item isn't added back to the folder yet, to // to correspond to the animation of the icon back into the folder. This is - public void hideItem(WorkspaceItemInfo info) { + public void hideItem(ItemInfo info) { View v = getViewForInfo(info); if (v != null) { v.setVisibility(INVISIBLE); } } - public void showItem(WorkspaceItemInfo info) { + public void showItem(ItemInfo info) { View v = getViewForInfo(info); if (v != null) { v.setVisibility(VISIBLE); @@ -1391,7 +1402,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } @Override - public void onAdd(WorkspaceItemInfo item, int rank) { + public void onAdd(ItemInfo item, int rank) { FolderGridOrganizer verifier = new FolderGridOrganizer( mActivityContext.getDeviceProfile()).setFolderInfo(mInfo); verifier.updateRankAndPos(item, rank); @@ -1406,7 +1417,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } @Override - public void onRemove(List items) { + public void onRemove(List items) { mItemsInvalidated = true; items.stream().map(this::getViewForInfo).forEach(mContent::removeItem); if (mState == STATE_ANIMATING) { @@ -1423,7 +1434,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo } } - private View getViewForInfo(final WorkspaceItemInfo item) { + private View getViewForInfo(final ItemInfo item) { return mContent.iterateOverItems((info, view) -> info == item); } @@ -1451,7 +1462,7 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo return mItemsInReadingOrder; } - public List getItemsOnPage(int page) { + public List getItemsOnPage(int page) { ArrayList allItems = getIconsInReadingOrder(); int lastPage = mContent.getPageCount() - 1; int totalItemsInFolder = allItems.size(); @@ -1463,9 +1474,9 @@ public class Folder extends AbstractFloatingView implements ClipPathView, DragSo int startIndex = page * itemsPerPage; int endIndex = Math.min(startIndex + numItemsOnCurrentPage, allItems.size()); - List itemsOnCurrentPage = new ArrayList<>(numItemsOnCurrentPage); + List itemsOnCurrentPage = new ArrayList<>(numItemsOnCurrentPage); for (int i = startIndex; i < endIndex; ++i) { - itemsOnCurrentPage.add((BubbleTextView) allItems.get(i)); + itemsOnCurrentPage.add(allItems.get(i)); } return itemsOnCurrentPage; } diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java index a91373ba49..7a2ec97f4d 100644 --- a/src/com/android/launcher3/folder/FolderAnimationManager.java +++ b/src/com/android/launcher3/folder/FolderAnimationManager.java @@ -43,6 +43,7 @@ import com.android.launcher3.R; import com.android.launcher3.ShortcutAndWidgetContainer; import com.android.launcher3.Utilities; import com.android.launcher3.anim.PropertyResetListener; +import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.util.Themes; import com.android.launcher3.views.BaseDragLayer; @@ -127,7 +128,7 @@ public class FolderAnimationManager { (BaseDragLayer.LayoutParams) mFolder.getLayoutParams(); mFolderIcon.getPreviewItemManager().recomputePreviewDrawingParams(); ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule(); - final List itemsInPreview = getPreviewIconsOnPage(0); + final List itemsInPreview = getPreviewIconsOnPage(0); // Match position of the FolderIcon final Rect folderIconPos = new Rect(); @@ -139,8 +140,8 @@ public class FolderAnimationManager { // Match size/scale of icons in the preview float previewScale = rule.scaleForItem(itemsInPreview.size()); float previewSize = rule.getIconSize() * previewScale; - float initialScale = previewSize / itemsInPreview.get(0).getIconSize() - * scaleRelativeToDragLayer; + float baseIconSize = getBubbleTextView(itemsInPreview.get(0)).getIconSize(); + float initialScale = previewSize / baseIconSize * scaleRelativeToDragLayer; final float finalScale = 1f; float scale = mIsOpening ? initialScale : finalScale; mFolder.setPivotX(0); @@ -198,11 +199,12 @@ public class FolderAnimationManager { // Initialize the Folder items' text. PropertyResetListener colorResetListener = new PropertyResetListener<>(TEXT_ALPHA_PROPERTY, 1f); - for (BubbleTextView icon : mFolder.getItemsOnPage(mFolder.mContent.getCurrentPage())) { + for (View icon : mFolder.getItemsOnPage(mFolder.mContent.getCurrentPage())) { + BubbleTextView titleText = getBubbleTextView(icon); if (mIsOpening) { - icon.setTextVisibility(false); + titleText.setTextVisibility(false); } - ObjectAnimator anim = icon.createTextAlphaAnimator(mIsOpening); + ObjectAnimator anim = titleText.createTextAlphaAnimator(mIsOpening); anim.addListener(colorResetListener); play(a, anim); } @@ -339,7 +341,7 @@ public class FolderAnimationManager { /** * Returns the list of "preview items" on {@param page}. */ - private List getPreviewIconsOnPage(int page) { + private List getPreviewIconsOnPage(int page) { return mPreviewVerifier.setFolderInfo(mFolder.mInfo) .previewItemsForPage(page, mFolder.getIconsInReadingOrder()); } @@ -351,7 +353,7 @@ public class FolderAnimationManager { int previewItemOffsetX, int previewItemOffsetY) { ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule(); boolean isOnFirstPage = mFolder.mContent.getCurrentPage() == 0; - final List itemsInPreview = getPreviewIconsOnPage( + final List itemsInPreview = getPreviewIconsOnPage( isOnFirstPage ? 0 : mFolder.mContent.getCurrentPage()); final int numItemsInPreview = itemsInPreview.size(); final int numItemsInFirstPagePreview = isOnFirstPage @@ -361,48 +363,49 @@ public class FolderAnimationManager { ShortcutAndWidgetContainer cwc = mContent.getPageAt(0).getShortcutsAndWidgets(); for (int i = 0; i < numItemsInPreview; ++i) { - final BubbleTextView btv = itemsInPreview.get(i); - CellLayoutLayoutParams btvLp = (CellLayoutLayoutParams) btv.getLayoutParams(); + final View v = itemsInPreview.get(i); + CellLayoutLayoutParams vLp = (CellLayoutLayoutParams) v.getLayoutParams(); // Calculate the final values in the LayoutParams. - btvLp.isLockedToGrid = true; - cwc.setupLp(btv); + vLp.isLockedToGrid = true; + cwc.setupLp(v); // Match scale of icons in the preview of the items on the first page. float previewScale = rule.scaleForItem(numItemsInFirstPagePreview); float previewSize = rule.getIconSize() * previewScale; - float iconScale = previewSize / itemsInPreview.get(i).getIconSize(); + float baseIconSize = getBubbleTextView(v).getIconSize(); + float iconScale = previewSize / baseIconSize; final float initialScale = iconScale / folderScale; final float finalScale = 1f; float scale = mIsOpening ? initialScale : finalScale; - btv.setScaleX(scale); - btv.setScaleY(scale); + v.setScaleX(scale); + v.setScaleY(scale); // Match positions of the icons in the folder with their positions in the preview rule.computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, mTmpParams); // The PreviewLayoutRule assumes that the icon size takes up the entire width so we // offset by the actual size. - int iconOffsetX = (int) ((btvLp.width - btv.getIconSize()) * iconScale) / 2; + int iconOffsetX = (int) ((vLp.width - baseIconSize) * iconScale) / 2; final int previewPosX = (int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale); - final float paddingTop = btv.getPaddingTop() * iconScale; + final float paddingTop = v.getPaddingTop() * iconScale; final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY - paddingTop) / folderScale); - final float xDistance = previewPosX - btvLp.x; - final float yDistance = previewPosY - btvLp.y; + final float xDistance = previewPosX - vLp.x; + final float yDistance = previewPosY - vLp.y; - Animator translationX = getAnimator(btv, View.TRANSLATION_X, xDistance, 0f); + Animator translationX = getAnimator(v, View.TRANSLATION_X, xDistance, 0f); translationX.setInterpolator(previewItemInterpolator); play(animatorSet, translationX); - Animator translationY = getAnimator(btv, View.TRANSLATION_Y, yDistance, 0f); + Animator translationY = getAnimator(v, View.TRANSLATION_Y, yDistance, 0f); translationY.setInterpolator(previewItemInterpolator); play(animatorSet, translationY); - Animator scaleAnimator = getAnimator(btv, SCALE_PROPERTY, initialScale, finalScale); + Animator scaleAnimator = getAnimator(v, SCALE_PROPERTY, initialScale, finalScale); scaleAnimator.setInterpolator(previewItemInterpolator); play(animatorSet, scaleAnimator); @@ -426,20 +429,20 @@ public class FolderAnimationManager { super.onAnimationStart(animation); // Necessary to initialize values here because of the start delay. if (mIsOpening) { - btv.setTranslationX(xDistance); - btv.setTranslationY(yDistance); - btv.setScaleX(initialScale); - btv.setScaleY(initialScale); + v.setTranslationX(xDistance); + v.setTranslationY(yDistance); + v.setScaleX(initialScale); + v.setScaleY(initialScale); } } @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); - btv.setTranslationX(0.0f); - btv.setTranslationY(0.0f); - btv.setScaleX(1f); - btv.setScaleY(1f); + v.setTranslationX(0.0f); + v.setTranslationY(0.0f); + v.setScaleX(1f); + v.setScaleY(1f); } }); } @@ -482,4 +485,15 @@ public class FolderAnimationManager { ? ObjectAnimator.ofArgb(drawable, property, v1, v2) : ObjectAnimator.ofArgb(drawable, property, v2, v1); } + + /** + * Gets the {@link com.android.launcher3.BubbleTextView} from an icon. In some cases the + * BubbleTextView is the whole icon itself, while in others it is contained within the view and + * only serves to store the title text. + */ + private BubbleTextView getBubbleTextView(View v) { + return v instanceof AppPairIcon + ? ((AppPairIcon) v).getTitleTextView() + : (BubbleTextView) v; + } } diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java index 62ce311c3b..6b30b95278 100644 --- a/src/com/android/launcher3/folder/FolderIcon.java +++ b/src/com/android/launcher3/folder/FolderIcon.java @@ -71,6 +71,7 @@ import com.android.launcher3.logger.LauncherAtom.FromState; import com.android.launcher3.logger.LauncherAtom.ToState; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; +import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.FolderInfo.FolderListener; import com.android.launcher3.model.data.FolderInfo.LabelState; @@ -118,7 +119,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel ClippedFolderIconLayoutRule mPreviewLayoutRule; private PreviewItemManager mPreviewItemManager; private PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0); - private List mCurrentPreviewItems = new ArrayList<>(); + private List mCurrentPreviewItems = new ArrayList<>(); boolean mAnimating = false; @@ -215,7 +216,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel // Keep the notification dot up to date with the sum of all the content's dots. FolderDotInfo folderDotInfo = new FolderDotInfo(); - for (WorkspaceItemInfo si : folderInfo.getContents()) { + for (ItemInfo si : folderInfo.getContents()) { folderDotInfo.addDotInfo(activity.getDotInfoForItem(si)); } icon.setDotInfo(folderDotInfo); @@ -261,20 +262,18 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel private boolean willAcceptItem(ItemInfo item) { final int itemType = item.itemType; - return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || - itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) && - item != mInfo && !mFolder.isOpen()); + return (Folder.willAcceptItemType(itemType) && item != mInfo && !mFolder.isOpen()); } public boolean acceptDrop(ItemInfo dragInfo) { return !mFolder.isDestroyed() && willAcceptItem(dragInfo); } - public void addItem(WorkspaceItemInfo item) { + public void addItem(ItemInfo item) { mInfo.add(item, true); } - public void removeItem(WorkspaceItemInfo item, boolean animate) { + public void removeItem(ItemInfo item, boolean animate) { mInfo.remove(item, animate); } @@ -287,8 +286,8 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel mOpenAlarm.setOnAlarmListener(mOnOpenListener); if (SPRING_LOADING_ENABLED && ((dragInfo instanceof WorkspaceItemFactory) - || (dragInfo instanceof WorkspaceItemInfo) - || (dragInfo instanceof PendingAddShortcutInfo))) { + || (dragInfo instanceof PendingAddShortcutInfo) + || Folder.willAccept(dragInfo))) { mOpenAlarm.setAlarm(ON_OPEN_DELAY); } } @@ -303,8 +302,8 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel return mPreviewItemManager.prepareCreateAnimation(destView); } - public void performCreateAnimation(final WorkspaceItemInfo destInfo, final View destView, - final WorkspaceItemInfo srcInfo, final DragObject d, Rect dstRect, + public void performCreateAnimation(final ItemInfo destInfo, final View destView, + final ItemInfo srcInfo, final DragObject d, Rect dstRect, float scaleRelativeToDragLayer) { final DragView srcView = d.dragView; prepareCreateAnimation(destView); @@ -330,7 +329,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel mOpenAlarm.cancelAlarm(); } - private void onDrop(final WorkspaceItemInfo item, DragObject d, Rect finalRect, + private void onDrop(final ItemInfo item, DragObject d, Rect finalRect, float scaleRelativeToDragLayer, int index, boolean itemReturnedOnFailedDrop) { item.cellX = -1; item.cellY = -1; @@ -361,7 +360,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel int numItemsInPreview = Math.min(MAX_NUM_ITEMS_IN_PREVIEW, index + 1); boolean itemAdded = false; if (itemReturnedOnFailedDrop || index >= MAX_NUM_ITEMS_IN_PREVIEW) { - List oldPreviewItems = new ArrayList<>(mCurrentPreviewItems); + List oldPreviewItems = new ArrayList<>(mCurrentPreviewItems); mInfo.add(item, index, false); mCurrentPreviewItems.clear(); mCurrentPreviewItems.addAll(getPreviewItemsOnPage(0)); @@ -422,7 +421,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel FolderNameInfos nameInfos = new FolderNameInfos(); Executors.MODEL_EXECUTOR.post(() -> { d.folderNameProvider.getSuggestedFolderName( - getContext(), mInfo.getContents(), nameInfos); + getContext(), mInfo.getAppContents(), nameInfos); postDelayed(() -> { setLabelSuggestion(nameInfos, d.logInstanceId); invalidate(); @@ -475,15 +474,21 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel public void onDrop(DragObject d, boolean itemReturnedOnFailedDrop) { - WorkspaceItemInfo item; + ItemInfo item; if (d.dragInfo instanceof WorkspaceItemFactory) { // Came from all apps -- make a copy item = ((WorkspaceItemFactory) d.dragInfo).makeWorkspaceItem(getContext()); } else if (d.dragSource instanceof BaseItemDragListener){ // Came from a different window -- make a copy - item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo); + if (d.dragInfo instanceof AppPairInfo) { + // dragged item is app pair + item = new AppPairInfo((AppPairInfo) d.dragInfo); + } else { + // dragged item is WorkspaceItemInfo + item = new WorkspaceItemInfo((WorkspaceItemInfo) d.dragInfo); + } } else { - item = (WorkspaceItemInfo) d.dragInfo; + item = d.dragInfo; } mFolder.notifyDrop(); onDrop(item, d, null, 1.0f, @@ -665,7 +670,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel /** * Returns the list of items which should be visible in the preview */ - public List getPreviewItemsOnPage(int page) { + public List getPreviewItemsOnPage(int page) { return mPreviewVerifier.setFolderInfo(mInfo).previewItemsForPage(page, mInfo.getContents()); } @@ -690,12 +695,12 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel /** * Updates the preview items which match the provided condition */ - public void updatePreviewItems(Predicate itemCheck) { + public void updatePreviewItems(Predicate itemCheck) { mPreviewItemManager.updatePreviewItems(itemCheck); } @Override - public void onAdd(WorkspaceItemInfo item, int rank) { + public void onAdd(ItemInfo item, int rank) { updatePreviewItems(false); boolean wasDotted = mDotInfo.hasDot(); mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item)); @@ -707,7 +712,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel } @Override - public void onRemove(List items) { + public void onRemove(List items) { updatePreviewItems(false); boolean wasDotted = mDotInfo.hasDot(); items.stream().map(mActivity::getDotInfoForItem).forEach(mDotInfo::subtractDotInfo); diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java index f2bed925c1..8eaa0dceab 100644 --- a/src/com/android/launcher3/folder/FolderPagedView.java +++ b/src/com/android/launcher3/folder/FolderPagedView.java @@ -41,8 +41,10 @@ import com.android.launcher3.PagedView; import com.android.launcher3.R; import com.android.launcher3.ShortcutAndWidgetContainer; import com.android.launcher3.Utilities; +import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.celllayout.CellLayoutLayoutParams; import com.android.launcher3.keyboard.ViewGroupFocusHelper; +import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pageindicators.PageIndicatorDots; @@ -148,7 +150,7 @@ public class FolderPagedView extends PagedView implements Cli /** * Binds items to the layout. */ - public void bindItems(List items) { + public void bindItems(List items) { if (mViewsBound) { unbindItems(); } @@ -164,8 +166,11 @@ public class FolderPagedView extends PagedView implements Cli CellLayout page = (CellLayout) getChildAt(i); ShortcutAndWidgetContainer container = page.getShortcutsAndWidgets(); for (int j = container.getChildCount() - 1; j >= 0; j--) { - container.getChildAt(j).setVisibility(View.VISIBLE); - mViewCache.recycleView(R.layout.folder_application, container.getChildAt(j)); + View iconView = container.getChildAt(j); + iconView.setVisibility(View.VISIBLE); + if (iconView instanceof BubbleTextView) { + mViewCache.recycleView(R.layout.folder_application, iconView); + } } page.removeAllViews(); mViewCache.recycleView(R.layout.folder_page, page); @@ -185,7 +190,7 @@ public class FolderPagedView extends PagedView implements Cli * Creates and adds an icon corresponding to the provided rank * @return the created icon */ - public View createAndAddViewForRank(WorkspaceItemInfo item, int rank) { + public View createAndAddViewForRank(ItemInfo item, int rank) { View icon = createNewView(item); if (!mViewsBound) { return icon; @@ -200,7 +205,7 @@ public class FolderPagedView extends PagedView implements Cli * Adds the {@param view} to the layout based on {@param rank} and updated the position * related attributes. It assumes that {@param item} is already attached to the view. */ - public void addViewForRank(View view, WorkspaceItemInfo item, int rank) { + public void addViewForRank(View view, ItemInfo item, int rank) { int pageNo = rank / mOrganizer.getMaxItemsPerPage(); CellLayoutLayoutParams lp = (CellLayoutLayoutParams) view.getLayoutParams(); @@ -209,26 +214,36 @@ public class FolderPagedView extends PagedView implements Cli } @SuppressLint("InflateParams") - public View createNewView(WorkspaceItemInfo item) { + public View createNewView(ItemInfo item) { if (item == null) { return null; } - final BubbleTextView textView = mViewCache.getView( - R.layout.folder_application, getContext(), null); - textView.applyFromWorkspaceItem(item); - textView.setOnClickListener(mFolder.mActivityContext.getItemOnClickListener()); - textView.setOnLongClickListener(mFolder); - textView.setOnFocusChangeListener(mFocusIndicatorHelper); - CellLayoutLayoutParams lp = (CellLayoutLayoutParams) textView.getLayoutParams(); + + final View icon; + if (item instanceof AppPairInfo api) { + // TODO (b/332607759): Make view cache work with app pair icons + icon = AppPairIcon.inflateIcon(R.layout.folder_app_pair, ActivityContext.lookupContext( + getContext()), null , api, BubbleTextView.DISPLAY_FOLDER); + } else { + icon = mViewCache.getView(R.layout.folder_application, getContext(), null); + ((BubbleTextView) icon).applyFromWorkspaceItem((WorkspaceItemInfo) item); + } + + icon.setOnClickListener(mFolder.mActivityContext.getItemOnClickListener()); + icon.setOnLongClickListener(mFolder); + icon.setOnFocusChangeListener(mFocusIndicatorHelper); + + CellLayoutLayoutParams lp = (CellLayoutLayoutParams) icon.getLayoutParams(); if (lp == null) { - textView.setLayoutParams(new CellLayoutLayoutParams( + icon.setLayoutParams(new CellLayoutLayoutParams( item.cellX, item.cellY, item.spanX, item.spanY)); } else { lp.setCellX(item.cellX); lp.setCellY(item.cellY); lp.cellHSpan = lp.cellVSpan = 1; } - return textView; + + return icon; } @Nullable @@ -497,13 +512,20 @@ public class FolderPagedView extends PagedView implements Cli if (page != null) { ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets(); for (int i = parent.getChildCount() - 1; i >= 0; i--) { - BubbleTextView icon = ((BubbleTextView) parent.getChildAt(i)); - icon.verifyHighRes(); + View iconView = parent.getChildAt(i); + Drawable d = null; + if (iconView instanceof BubbleTextView btv) { + btv.verifyHighRes(); + d = btv.getIcon(); + } else if (iconView instanceof AppPairIcon api) { + api.verifyHighRes(); + d = api.getIconDrawableArea().getDrawable(); + } + // Set the callback back to the actual icon, in case // it was captured by the FolderIcon - Drawable d = icon.getIcon(); if (d != null) { - d.setCallback(icon); + d.setCallback(iconView); } } } diff --git a/src/com/android/launcher3/folder/LauncherDelegate.java b/src/com/android/launcher3/folder/LauncherDelegate.java index 33bcf217de..07215c4710 100644 --- a/src/com/android/launcher3/folder/LauncherDelegate.java +++ b/src/com/android/launcher3/folder/LauncherDelegate.java @@ -33,7 +33,7 @@ import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.model.ModelWriter; import com.android.launcher3.model.data.FolderInfo; -import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.BaseDragLayer; @@ -86,7 +86,7 @@ public class LauncherDelegate { FolderInfo info = folder.mInfo; if (itemCount <= 1) { View newIcon = null; - WorkspaceItemInfo finalItem = null; + ItemInfo finalItem = null; if (itemCount == 1) { // Move the item from the folder to the workspace, in the position of the diff --git a/src/com/android/launcher3/folder/PreviewItemDrawingParams.java b/src/com/android/launcher3/folder/PreviewItemDrawingParams.java index 58efdc12ce..0faa1c9a23 100644 --- a/src/com/android/launcher3/folder/PreviewItemDrawingParams.java +++ b/src/com/android/launcher3/folder/PreviewItemDrawingParams.java @@ -17,7 +17,7 @@ package com.android.launcher3.folder; import android.graphics.drawable.Drawable; -import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.model.data.ItemInfo; /** * Manages the parameters used to draw a Folder preview item. @@ -30,7 +30,7 @@ class PreviewItemDrawingParams { public FolderPreviewItemAnim anim; public boolean hidden; public Drawable drawable; - public WorkspaceItemInfo item; + public ItemInfo item; PreviewItemDrawingParams(float transX, float transY, float scale) { this.transX = transX; diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java index 9001a0c271..63116384c3 100644 --- a/src/com/android/launcher3/folder/PreviewItemManager.java +++ b/src/com/android/launcher3/folder/PreviewItemManager.java @@ -16,6 +16,7 @@ package com.android.launcher3.folder; +import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ENTER_INDEX; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDEX; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; @@ -41,7 +42,12 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.BubbleTextView; import com.android.launcher3.Utilities; +import com.android.launcher3.apppairs.AppPairIcon; +import com.android.launcher3.apppairs.AppPairIconDrawingParams; +import com.android.launcher3.apppairs.AppPairIconGraphic; import com.android.launcher3.graphics.PreloadIconDrawable; +import com.android.launcher3.model.data.AppPairInfo; +import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.Themes; @@ -125,7 +131,9 @@ public class PreviewItemManager { } Drawable prepareCreateAnimation(final View destView) { - Drawable animateDrawable = ((BubbleTextView) destView).getIcon(); + Drawable animateDrawable = destView instanceof AppPairIcon + ? ((AppPairIcon) destView).getIconDrawableArea().getDrawable() + : ((BubbleTextView) destView).getIcon(); computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), destView.getMeasuredWidth()); mReferenceDrawable = animateDrawable; @@ -258,7 +266,7 @@ public class PreviewItemManager { } void buildParamsForPage(int page, ArrayList params, boolean animate) { - List items = mIcon.getPreviewItemsOnPage(page); + List items = mIcon.getPreviewItemsOnPage(page); // We adjust the size of the list to match the number of items in the preview. while (items.size() < params.size()) { @@ -328,16 +336,18 @@ public class PreviewItemManager { mNumOfPrevItems = numOfPrevItemsAux; } - void updatePreviewItems(Predicate itemCheck) { + void updatePreviewItems(Predicate itemCheck) { boolean modified = false; for (PreviewItemDrawingParams param : mFirstPageParams) { - if (itemCheck.test(param.item)) { + if (itemCheck.test(param.item) + || (param.item instanceof AppPairInfo api && api.anyMatch(itemCheck))) { setDrawable(param, param.item); modified = true; } } for (PreviewItemDrawingParams param : mCurrentPageParams) { - if (itemCheck.test(param.item)) { + if (itemCheck.test(param.item) + || (param.item instanceof AppPairInfo api && api.anyMatch(itemCheck))) { setDrawable(param, param.item); modified = true; } @@ -370,15 +380,14 @@ public class PreviewItemManager { * @param newItems The list of items in the new preview. * @param dropped The item that was dropped onto the FolderIcon. */ - public void onDrop(List oldItems, List newItems, - WorkspaceItemInfo dropped) { + public void onDrop(List oldItems, List newItems, ItemInfo dropped) { int numItems = newItems.size(); final ArrayList params = mFirstPageParams; buildParamsForPage(0, params, false); // New preview items for items that are moving in (except for the dropped item). - List moveIn = new ArrayList<>(); - for (WorkspaceItemInfo newItem : newItems) { + List moveIn = new ArrayList<>(); + for (ItemInfo newItem : newItems) { if (!oldItems.contains(newItem) && !newItem.equals(dropped)) { moveIn.add(newItem); } @@ -401,10 +410,10 @@ public class PreviewItemManager { } // Old preview items that need to be moved out. - List moveOut = new ArrayList<>(oldItems); + List moveOut = new ArrayList<>(oldItems); moveOut.removeAll(newItems); for (int i = 0; i < moveOut.size(); ++i) { - WorkspaceItemInfo item = moveOut.get(i); + ItemInfo item = moveOut.get(i); int oldIndex = oldItems.indexOf(item); PreviewItemDrawingParams p = computePreviewItemDrawingParams(oldIndex, numItems, null); updateTransitionParam(p, item, oldIndex, EXIT_INDEX, numItems); @@ -418,7 +427,7 @@ public class PreviewItemManager { } } - private void updateTransitionParam(final PreviewItemDrawingParams p, WorkspaceItemInfo item, + private void updateTransitionParam(final PreviewItemDrawingParams p, ItemInfo item, int prevIndex, int newIndex, int numItems) { setDrawable(p, item); @@ -431,16 +440,24 @@ public class PreviewItemManager { } @VisibleForTesting - public void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) { - if (item.hasPromiseIconUi() || (item.runtimeStatusFlags - & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { - PreloadIconDrawable drawable = newPendingIcon(mContext, item); - p.drawable = drawable; - } else { - p.drawable = item.newIcon(mContext, - Themes.isThemedIconEnabled(mContext) ? FLAG_THEMED : 0); + public void setDrawable(PreviewItemDrawingParams p, ItemInfo item) { + if (item instanceof WorkspaceItemInfo wii) { + if (wii.hasPromiseIconUi() || (wii.runtimeStatusFlags + & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) { + PreloadIconDrawable drawable = newPendingIcon(mContext, wii); + p.drawable = drawable; + } else { + p.drawable = wii.newIcon(mContext, + Themes.isThemedIconEnabled(mContext) ? FLAG_THEMED : 0); + } + p.drawable.setBounds(0, 0, mIconSize, mIconSize); + } else if (item instanceof AppPairInfo api) { + AppPairIconDrawingParams appPairParams = + new AppPairIconDrawingParams(mContext, DISPLAY_FOLDER); + p.drawable = AppPairIconGraphic.composeDrawable(api, appPairParams); + p.drawable.setBounds(0, 0, mIconSize, mIconSize); } - p.drawable.setBounds(0, 0, mIconSize, mIconSize); + p.item = item; // Set the callback to FolderIcon as it is responsible to drawing the icon. The // callback will be released when the folder is opened. diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java index 44e45da7af..d5de4cee5e 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -44,6 +44,7 @@ import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Workspace; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.AppPairInfo; import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; @@ -259,10 +260,15 @@ public class BgDataModel { itemsIdMap.put(item.id, item); switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: - case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: - collections.put(item.id, (CollectionInfo) item); + collections.put(item.id, (FolderInfo) item); workspaceItems.add(item); break; + case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: + collections.put(item.id, (AppPairInfo) item); + // Fall through here. App pairs are both containers (like folders) and containable + // items (can be placed in folders). So we need to add app pairs to the folders + // array (above) but also verify the existence of their container, like regular + // apps (below). case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP || @@ -277,7 +283,7 @@ public class BgDataModel { Log.e(TAG, msg); } } else { - findOrMakeFolder(item.container).add((WorkspaceItemInfo) item); + findOrMakeFolder(item.container).add(item); } } break; diff --git a/src/com/android/launcher3/model/FirstScreenBroadcast.java b/src/com/android/launcher3/model/FirstScreenBroadcast.java index 1deb6657e1..cc20cd194c 100644 --- a/src/com/android/launcher3/model/FirstScreenBroadcast.java +++ b/src/com/android/launcher3/model/FirstScreenBroadcast.java @@ -115,7 +115,7 @@ public class FirstScreenBroadcast { for (ItemInfo info : firstScreenItems) { if (info instanceof CollectionInfo ci) { String collectionItemInfoPackage; - for (ItemInfo collectionItemInfo : cloneOnMainThread(ci.getContents())) { + for (ItemInfo collectionItemInfo : cloneOnMainThread(ci.getAppContents())) { collectionItemInfoPackage = getPackageName(collectionItemInfo); if (collectionItemInfoPackage != null && packages.contains(collectionItemInfoPackage)) { diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 30cccd5f47..e0ced83855 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -81,7 +81,6 @@ import com.android.launcher3.model.data.CollectionInfo; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.IconRequestInfo; import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.InstallSessionHelper; @@ -494,9 +493,7 @@ public class LoaderTask implements Runnable { } appPair.getContents().sort(Folder.ITEM_POS_COMPARATOR); - // Fetch hi-res icons if needed. - appPair.getContents().stream().filter(ItemInfoWithIcon::usingLowResIcon) - .forEach(member -> mIconCache.getTitleAndIcon(member, false)); + appPair.fetchHiResIconsIfNeeded(mIconCache); } } @@ -566,12 +563,16 @@ public class LoaderTask implements Runnable { // Ranks are the source of truth for folder items, so cellX and cellY can be // ignored for now. Database will be updated once user manually modifies folder. for (int rank = 0; rank < size; ++rank) { - WorkspaceItemInfo info = folder.getContents().get(rank); + ItemInfo info = folder.getContents().get(rank); info.rank = rank; - if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION + if (info instanceof WorkspaceItemInfo wii + && wii.usingLowResIcon() + && wii.itemType == Favorites.ITEM_TYPE_APPLICATION && verifiers.stream().anyMatch(it -> it.isItemInPreview(info.rank))) { - mIconCache.getTitleAndIcon(info, false); + mIconCache.getTitleAndIcon(wii, false); + } else if (info instanceof AppPairInfo api) { + api.fetchHiResIconsIfNeeded(mIconCache); } } } @@ -788,7 +789,7 @@ public class LoaderTask implements Runnable { FolderNameInfos suggestionInfos = new FolderNameInfos(); CollectionInfo info = mBgDataModel.collections.valueAt(i); if (info instanceof FolderInfo fi && fi.suggestedFolderNames == null) { - provider.getSuggestedFolderName(mApp.getContext(), fi.getContents(), + provider.getSuggestedFolderName(mApp.getContext(), fi.getAppContents(), suggestionInfos); fi.suggestedFolderNames = suggestionInfos; } diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt index aa2929096e..d27be727af 100644 --- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt +++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt @@ -375,7 +375,7 @@ class WorkspaceItemProcessor( val folderInfo: FolderInfo = collection val newAppPair = AppPairInfo() // Move the placeholder's contents over to the new app pair. - folderInfo.contents.forEach(newAppPair::add) + folderInfo.getContents().forEach(newAppPair::add) collection = newAppPair // Remove the placeholder and add the app pair into the data model. bgDataModel.collections.remove(c.id) diff --git a/src/com/android/launcher3/model/data/AppPairInfo.kt b/src/com/android/launcher3/model/data/AppPairInfo.kt index 408131683e..a56ee8ed2a 100644 --- a/src/com/android/launcher3/model/data/AppPairInfo.kt +++ b/src/com/android/launcher3/model/data/AppPairInfo.kt @@ -18,11 +18,14 @@ package com.android.launcher3.model.data import android.content.Context import com.android.launcher3.LauncherSettings +import com.android.launcher3.icons.IconCache import com.android.launcher3.logger.LauncherAtom import com.android.launcher3.views.ActivityContext /** A type of app collection that launches multiple apps into split screen. */ class AppPairInfo() : CollectionInfo() { + private var contents: ArrayList = ArrayList() + init { itemType = LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR } @@ -33,11 +36,28 @@ class AppPairInfo() : CollectionInfo() { add(app2) } - /** Adds an element to the contents array. */ - override fun add(item: WorkspaceItemInfo) { + /** Creates a new AppPairInfo that is a copy of the provided one. */ + constructor(appPairInfo: AppPairInfo) : this() { + contents = appPairInfo.contents.clone() as ArrayList + copyFrom(appPairInfo) + } + + /** Adds an element to the contents ArrayList. */ + override fun add(item: ItemInfo) { + if (item !is WorkspaceItemInfo) { + throw RuntimeException("tried to add an illegal type into an app pair") + } + contents.add(item) } + /** Returns the app pair's member apps as an ArrayList of [ItemInfo]. */ + override fun getContents(): ArrayList = + ArrayList(contents.stream().map { it as ItemInfo }.toList()) + + /** Returns the app pair's member apps as an ArrayList of [WorkspaceItemInfo]. */ + override fun getAppContents(): ArrayList = contents + /** Returns the first app in the pair. */ fun getFirstApp() = contents[0] @@ -50,7 +70,9 @@ class AppPairInfo() : CollectionInfo() { /** Checks if the app pair is launchable at the current screen size. */ fun isLaunchable(context: Context) = (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile().isTablet || - noneMatch { it.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE) } + getAppContents().stream().noneMatch { + it.hasStatusFlag(WorkspaceItemInfo.FLAG_NON_RESIZEABLE) + } /** Generates an ItemInfo for logging. */ override fun buildProto(cInfo: CollectionInfo?): LauncherAtom.ItemInfo { @@ -62,4 +84,11 @@ class AppPairInfo() : CollectionInfo() { .setContainerInfo(getContainerInfo()) .build() } + + /** Fetches high-res icons for member apps if needed. */ + fun fetchHiResIconsIfNeeded(iconCache: IconCache) { + getAppContents().stream().filter(ItemInfoWithIcon::usingLowResIcon).forEach { member -> + iconCache.getTitleAndIcon(member, false) + } + } } diff --git a/src/com/android/launcher3/model/data/CollectionInfo.kt b/src/com/android/launcher3/model/data/CollectionInfo.kt index 2b865a574e..4f5e12fd30 100644 --- a/src/com/android/launcher3/model/data/CollectionInfo.kt +++ b/src/com/android/launcher3/model/data/CollectionInfo.kt @@ -22,19 +22,21 @@ import com.android.launcher3.util.ContentWriter import java.util.function.Predicate abstract class CollectionInfo : ItemInfo() { - var contents: ArrayList = ArrayList() + /** Adds an ItemInfo to the collection. Throws if given an illegal type. */ + abstract fun add(item: ItemInfo) - abstract fun add(item: WorkspaceItemInfo) + /** Returns the collection's contents as an ArrayList of [ItemInfo]. */ + abstract fun getContents(): ArrayList + + /** + * Returns the collection's contents as an ArrayList of [WorkspaceItemInfo]. Does not include + * other collection [ItemInfo]s that are inside this collection; rather, it should collect + * *their* contents and adds them to the ArrayList. + */ + abstract fun getAppContents(): ArrayList /** Convenience function. Checks contents to see if any match a given predicate. */ - fun anyMatch(matcher: Predicate): Boolean { - return contents.stream().anyMatch(matcher) - } - - /** Convenience function. Returns true if none of the contents match a given predicate. */ - fun noneMatch(matcher: Predicate): Boolean { - return contents.stream().noneMatch(matcher) - } + fun anyMatch(matcher: Predicate) = getContents().stream().anyMatch(matcher) override fun onAddToDatabase(writer: ContentWriter) { super.onAddToDatabase(writer) diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java index 1bbb2fe971..56996ef21c 100644 --- a/src/com/android/launcher3/model/data/FolderInfo.java +++ b/src/com/android/launcher3/model/data/FolderInfo.java @@ -29,6 +29,7 @@ import androidx.annotation.Nullable; import com.android.launcher3.LauncherSettings; import com.android.launcher3.Utilities; +import com.android.launcher3.folder.Folder; import com.android.launcher3.folder.FolderNameInfos; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logger.LauncherAtom.Attribute; @@ -98,14 +99,20 @@ public class FolderInfo extends CollectionInfo { public FolderNameInfos suggestedFolderNames; + /** + * The apps and shortcuts + */ + private final ArrayList contents = new ArrayList<>(); + private ArrayList mListeners = new ArrayList<>(); public FolderInfo() { itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER; } - /** Adds a app or shortcut to the contents array without animation. */ - public void add(@NonNull WorkspaceItemInfo item) { + /** Adds a app or shortcut to the contents ArrayList without animation. */ + @Override + public void add(@NonNull ItemInfo item) { add(item, false /* animate */); } @@ -114,14 +121,18 @@ public class FolderInfo extends CollectionInfo { * * @param item */ - public void add(WorkspaceItemInfo item, boolean animate) { + public void add(ItemInfo item, boolean animate) { add(item, getContents().size(), animate); } /** * Add an app or shortcut for a specified rank. */ - public void add(WorkspaceItemInfo item, int rank, boolean animate) { + public void add(ItemInfo item, int rank, boolean animate) { + if (!Folder.willAccept(item)) { + throw new RuntimeException("tried to add an illegal type into a folder"); + } + rank = Utilities.boundToRange(rank, 0, getContents().size()); getContents().add(rank, item); for (int i = 0; i < mListeners.size(); i++) { @@ -135,21 +146,49 @@ public class FolderInfo extends CollectionInfo { * * @param item */ - public void remove(WorkspaceItemInfo item, boolean animate) { + public void remove(ItemInfo item, boolean animate) { removeAll(Collections.singletonList(item), animate); } /** * Remove all matching app or shortcut. Does not change the DB. */ - public void removeAll(List items, boolean animate) { - getContents().removeAll(items); + public void removeAll(List items, boolean animate) { + contents.removeAll(items); for (int i = 0; i < mListeners.size(); i++) { mListeners.get(i).onRemove(items); } itemsChanged(animate); } + /** + * Returns the folder's contents as an ArrayList of {@link ItemInfo}. Includes + * {@link WorkspaceItemInfo} and {@link AppPairInfo}s. + */ + @NonNull + @Override + public ArrayList getContents() { + return contents; + } + + /** + * Returns the folder's contents as an ArrayList of {@link WorkspaceItemInfo}. Note: Does not + * return any {@link AppPairInfo}s contained in the folder, instead collects *their* contents + * and adds them to the ArrayList. + */ + @Override + public ArrayList getAppContents() { + ArrayList workspaceItemInfos = new ArrayList<>(); + for (ItemInfo item : contents) { + if (item instanceof WorkspaceItemInfo wii) { + workspaceItemInfos.add(wii); + } else if (item instanceof AppPairInfo api) { + workspaceItemInfos.addAll(api.getAppContents()); + } + } + return workspaceItemInfos; + } + @Override public void onAddToDatabase(@NonNull ContentWriter writer) { super.onAddToDatabase(writer); @@ -171,8 +210,8 @@ public class FolderInfo extends CollectionInfo { } public interface FolderListener { - void onAdd(WorkspaceItemInfo item, int rank); - void onRemove(List item); + void onAdd(ItemInfo item, int rank); + void onRemove(List item); void onItemsChanged(boolean animate); } @@ -263,10 +302,17 @@ public class FolderInfo extends CollectionInfo { public ItemInfo makeShallowCopy() { FolderInfo folderInfo = new FolderInfo(); folderInfo.copyFrom(this); - folderInfo.setContents(this.getContents()); return folderInfo; } + @Override + public void copyFrom(@NonNull ItemInfo info) { + super.copyFrom(info); + if (info instanceof FolderInfo fi) { + contents.addAll(fi.getContents()); + } + } + /** * Returns index of the accepted suggestion. */ diff --git a/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt index 3a1883cb02..da14425df0 100644 --- a/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt +++ b/tests/src/com/android/launcher3/folder/PreviewItemManagerTest.kt @@ -29,7 +29,7 @@ import com.android.launcher3.icons.BaseIconFactory import com.android.launcher3.icons.FastBitmapDrawable import com.android.launcher3.icons.UserBadgeDrawable import com.android.launcher3.model.data.FolderInfo -import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.util.ActivityContextWrapper import com.android.launcher3.util.FlagOp import com.android.launcher3.util.LauncherLayoutBuilder @@ -47,7 +47,7 @@ class PreviewItemManagerTest { private lateinit var previewItemManager: PreviewItemManager private lateinit var context: Context - private lateinit var folderItems: ArrayList + private lateinit var folderItems: ArrayList private lateinit var modelHelper: LauncherModelHelper private lateinit var folderIcon: FolderIcon @@ -72,15 +72,17 @@ class PreviewItemManagerTest { .build() ) .loadModelSync() - folderItems = modelHelper.bgDataModel.collections.valueAt(0).contents + folderItems = modelHelper.bgDataModel.collections.valueAt(0).getContents() folderIcon.mInfo = modelHelper.bgDataModel.collections.valueAt(0) as FolderInfo - folderIcon.mInfo.contents = folderItems + folderIcon.mInfo.getContents().addAll(folderItems) + // Use getAppContents() to "cast" contents to WorkspaceItemInfo so we can set bitmaps + val folderApps = modelHelper.bgDataModel.collections.valueAt(0).getAppContents() // Set first icon to be themed. - folderItems[0] + folderApps[0] .bitmap .setMonoIcon( - folderItems[0].bitmap.icon, + folderApps[0].bitmap.icon, BaseIconFactory( context, context.resources.configuration.densityDpi, @@ -89,7 +91,7 @@ class PreviewItemManagerTest { ) // Set second icon to be non-themed. - folderItems[1] + folderApps[1] .bitmap .setMonoIcon( null, @@ -101,23 +103,21 @@ class PreviewItemManagerTest { ) // Set third icon to be themed with badge. - folderItems[2] + folderApps[2] .bitmap .setMonoIcon( - folderItems[2].bitmap.icon, + folderApps[2].bitmap.icon, BaseIconFactory( context, context.resources.configuration.densityDpi, previewItemManager.mIconSize ) ) - folderItems[2].bitmap = - folderItems[2].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK)) + folderApps[2].bitmap = folderApps[2].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK)) // Set fourth icon to be non-themed with badge. - folderItems[3].bitmap = - folderItems[3].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK)) - folderItems[3] + folderApps[3].bitmap = folderApps[3].bitmap.withFlags(profileFlagOp(UserIconInfo.TYPE_WORK)) + folderApps[3] .bitmap .setMonoIcon( null, diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java index abb0c39d56..d3a63558d8 100644 --- a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java +++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java @@ -160,6 +160,6 @@ public class CacheDataUpdatedTaskTest { } private List allItems() { - return ((FolderInfo) mModelHelper.getBgDataModel().itemsIdMap.get(1)).getContents(); + return ((FolderInfo) mModelHelper.getBgDataModel().itemsIdMap.get(1)).getAppContents(); } } diff --git a/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt b/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt index ed587a1d33..2e209a48c4 100644 --- a/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt +++ b/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt @@ -168,8 +168,8 @@ class FolderIconLoadTest { val collections = modelHelper.getBgDataModel().collections assertThat(collections.size()).isEqualTo(1) - assertThat(collections.valueAt(0).contents.size).isEqualTo(itemCount) - return collections.valueAt(0).contents + assertThat(collections.valueAt(0).getAppContents().size).isEqualTo(itemCount) + return collections.valueAt(0).getAppContents() } private fun verifyHighRes(items: ArrayList, vararg indices: Int) { diff --git a/tests/src/com/android/launcher3/util/ItemInflaterTest.kt b/tests/src/com/android/launcher3/util/ItemInflaterTest.kt index 00655276b9..dae09bb595 100644 --- a/tests/src/com/android/launcher3/util/ItemInflaterTest.kt +++ b/tests/src/com/android/launcher3/util/ItemInflaterTest.kt @@ -139,9 +139,9 @@ class ItemInflaterTest { @Test fun test_folder_inflated_on_UI() { val itemInfo = FolderInfo() - itemInfo.contents.add(workspaceItemInfo()) - itemInfo.contents.add(workspaceItemInfo()) - itemInfo.contents.add(workspaceItemInfo()) + itemInfo.add(workspaceItemInfo()) + itemInfo.add(workspaceItemInfo()) + itemInfo.add(workspaceItemInfo()) val view = MAIN_EXECUTOR.submit(Callable { underTest.inflateItem(itemInfo, modelWriter) }).get() @@ -155,9 +155,9 @@ class ItemInflaterTest { setFlagsRule.enableFlags(Flags.FLAG_ENABLE_WORKSPACE_INFLATION) val itemInfo = FolderInfo() - itemInfo.contents.add(workspaceItemInfo()) - itemInfo.contents.add(workspaceItemInfo()) - itemInfo.contents.add(workspaceItemInfo()) + itemInfo.add(workspaceItemInfo()) + itemInfo.add(workspaceItemInfo()) + itemInfo.add(workspaceItemInfo()) val view = VIEW_PREINFLATION_EXECUTOR.submit( @@ -173,8 +173,8 @@ class ItemInflaterTest { fun test_app_pair_inflated_on_UI() { val itemInfo = AppPairInfo() itemInfo.itemType = ITEM_TYPE_APP_PAIR - itemInfo.contents.add(workspaceItemInfo()) - itemInfo.contents.add(workspaceItemInfo()) + itemInfo.add(workspaceItemInfo()) + itemInfo.add(workspaceItemInfo()) val view = MAIN_EXECUTOR.submit(Callable { underTest.inflateItem(itemInfo, modelWriter) }).get() @@ -189,8 +189,8 @@ class ItemInflaterTest { val itemInfo = AppPairInfo() itemInfo.itemType = ITEM_TYPE_APP_PAIR - itemInfo.contents.add(workspaceItemInfo()) - itemInfo.contents.add(workspaceItemInfo()) + itemInfo.add(workspaceItemInfo()) + itemInfo.add(workspaceItemInfo()) val view = VIEW_PREINFLATION_EXECUTOR.submit( From 7d0edb46ac0a09aa0c801598b46cd338d04886a9 Mon Sep 17 00:00:00 2001 From: Alex Chau Date: Wed, 3 Apr 2024 13:41:20 +0000 Subject: [PATCH 17/20] Reland "Update Split button visibility based on DeviceProfile change" This reverts commit 38bc885de9407f7e50e76fbe6470f8425cb5f61b. - Always request layout of action_buttons after changing visibility of its children - Update Split button visibility based on DeviceProfile change in updateDimension() only - Update Split button visibility based on 3P launcher in initialization only - Also simplified action_buttons to wrap_content and layout in middle of parent - Also removed the space between buttons and use marginStart - Fixed TAPL to not expect save app pair button on phone. Before this CL actions_buttons are still on view hierarchy despite they're not visible on screen. Fix: 321291049 Fix: 329255757 Test: Clear all tasks, fold, launch app, swipe up to Overivew; repeat in RTL Test: OverviewImageTest Flag: None Change-Id: I9ecf872279f6f07d2d9bc33fb09031568023cb77 --- .../res/layout/overview_actions_container.xml | 12 ++----- .../quickstep/views/OverviewActionsView.java | 35 ++++++++++++------- .../android/quickstep/views/RecentsView.java | 6 ++-- .../android/launcher3/tapl/BaseOverview.java | 5 +-- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml index 5d489f5f34..d086da481d 100644 --- a/quickstep/res/layout/overview_actions_container.xml +++ b/quickstep/res/layout/overview_actions_container.xml @@ -21,10 +21,9 @@