feat: Add option to clear home screen in settings (#6125)

Signed-off-by: abhixv <abhi.sharma1@hotmail.com>
This commit is contained in:
Abhishek Sharma
2025-12-20 03:26:30 +05:30
committed by Pun Butrach
parent 9898749619
commit 5f3a03f4fb
1577 changed files with 112563 additions and 80248 deletions
@@ -27,7 +27,7 @@ import android.animation.AnimatorSet;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import android.os.RemoteException;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
@@ -223,7 +223,6 @@ public class LauncherAnimationRunner extends RemoteAnimationRunnerCompat {
if (skipFirstFrame) {
// Because t=0 has the app icon in its original spot, we can skip the
// first frame and have the same movement one frame earlier.
Log.d("b/311077782", "LauncherAnimationRunner.setAnimation");
mAnimator.setCurrentPlayTime(
Math.min(getSingleFrameMs(context), mAnimator.getTotalDuration()));
}
@@ -238,7 +237,7 @@ public class LauncherAnimationRunner extends RemoteAnimationRunnerCompat {
* animation finished runnable.
*/
@Override
public void onAnimationFinished() {
public void onAnimationFinished() throws RemoteException {
mASyncFinishRunnable.run();
}
}
@@ -15,11 +15,11 @@
*/
package com.android.launcher3;
import com.android.quickstep.util.ContextInitListener;
import com.android.quickstep.util.ActivityInitListener;
import java.util.function.BiPredicate;
public class LauncherInitListener extends ContextInitListener<Launcher> {
public class LauncherInitListener extends ActivityInitListener<Launcher> {
/**
* @param onInitListener a callback made when the activity is initialized. The callback should
@@ -31,8 +31,8 @@ public class LauncherInitListener extends ContextInitListener<Launcher> {
}
@Override
public boolean handleInit(Launcher launcher, boolean isHomeStarted) {
public boolean handleInit(Launcher launcher, boolean alreadyOnHome) {
launcher.deferOverlayCallbacksUntilNextResumeOrStop();
return super.handleInit(launcher, isHomeStarted);
return super.handleInit(launcher, alreadyOnHome);
}
}
@@ -15,21 +15,10 @@
*/
package com.android.launcher3;
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import android.view.KeyEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.allapps.AllAppsRecyclerView;
import com.android.launcher3.allapps.SearchRecyclerView;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.uioverrides.PredictedAppIcon;
import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -37,64 +26,13 @@ import com.android.launcher3.uioverrides.QuickstepLauncher;
import java.util.List;
public class QuickstepAccessibilityDelegate extends LauncherAccessibilityDelegate {
private QuickstepLauncher mLauncher;
public QuickstepAccessibilityDelegate(QuickstepLauncher launcher) {
super(launcher);
mLauncher = launcher;
mActions.put(PIN_PREDICTION, new LauncherAction(
PIN_PREDICTION, R.string.pin_prediction, KeyEvent.KEYCODE_P));
}
@Override
public void onPopulateAccessibilityEvent(View view, AccessibilityEvent event) {
super.onPopulateAccessibilityEvent(view, event);
// Scroll to the position if focused view in main allapps list and not completely visible.
// Gate based on TYPE_VIEW_ACCESSIBILITY_FOCUSED for unintended scrolling with external
// mouse.
if (event.getEventType() == TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
scrollToPositionIfNeeded(view);
}
}
private void scrollToPositionIfNeeded(View view) {
if (!Flags.accessibilityScrollOnAllapps()) {
return;
}
AllAppsRecyclerView contentView = mLauncher.getAppsView().getActiveRecyclerView();
if (contentView instanceof SearchRecyclerView) {
return;
}
LinearLayoutManager layoutManager = (LinearLayoutManager) contentView.getLayoutManager();
if (layoutManager == null) {
return;
}
RecyclerView.ViewHolder vh = contentView.findContainingViewHolder(view);
if (vh == null) {
return;
}
int itemPosition = vh.getBindingAdapterPosition();
if (itemPosition == NO_POSITION) {
return;
}
int firstCompletelyVisible = layoutManager.findFirstCompletelyVisibleItemPosition();
int lastCompletelyVisible = layoutManager.findLastCompletelyVisibleItemPosition();
boolean itemCompletelyVisible = firstCompletelyVisible <= itemPosition
&& lastCompletelyVisible >= itemPosition;
if (itemCompletelyVisible) {
return;
}
RecyclerView.SmoothScroller smoothScroller =
new LinearSmoothScroller(mLauncher.asContext()) {
@Override
protected int getVerticalSnapPreference() {
return LinearSmoothScroller.SNAP_TO_ANY;
}
};
smoothScroller.setTargetPosition(itemPosition);
layoutManager.startSmoothScroll(smoothScroller);
}
@Override
protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
if (host instanceof PredictedAppIcon && !((PredictedAppIcon) host).isPinned()) {
File diff suppressed because it is too large Load Diff
@@ -23,9 +23,6 @@ import static android.view.WindowInsets.Type.statusBars;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ClipData;
@@ -36,40 +33,32 @@ import android.util.Log;
import android.view.View;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.window.BackEvent;
import android.window.OnBackAnimationCallback;
import android.window.OnBackInvokedDispatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.dragndrop.SimpleDragLayer;
import com.android.launcher3.model.StringCache;
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.model.data.PackageItemInfo;
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.WidgetsListBaseEntriesBuilder;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.picker.WidgetCategoryFilter;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/** An Activity that can host Launcher's widget picker. */
public class WidgetPickerActivity extends BaseActivity implements
WidgetPredictionsRequester.WidgetPredictionsListener {
public class WidgetPickerActivity extends BaseActivity {
private static final String TAG = "WidgetPickerActivity";
/**
* Name of the extra that indicates that a widget being dragged.
@@ -83,24 +72,11 @@ public class WidgetPickerActivity extends BaseActivity implements
// 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";
// Unlike the AppWidgetManager.EXTRA_CATEGORY_FILTER, this filter removes certain categories.
// Filter is ignore if it is not a negative value.
// Example usage: WIDGET_CATEGORY_HOME_SCREEN.inv() and WIDGET_CATEGORY_NOT_KEYGUARD.inv()
private static final String EXTRA_CATEGORY_EXCLUSION_FILTER = "category_exclusion_filter";
/**
* Widgets currently added by the user in the UI surface.
* <p>This allows widget picker to exclude existing widgets from suggestions.</p>
*/
private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets";
/**
* Intent extra for the string representing the title displayed within the picker header.
*/
private static final String EXTRA_PICKER_TITLE = "picker_title";
/**
* Intent extra for the string representing the description displayed within the picker header.
*/
private static final String EXTRA_PICKER_DESCRIPTION = "picker_description";
/**
* A unique identifier of the surface hosting the widgets;
* <p>"widgets" is reserved for home screen surface.</p>
@@ -109,52 +85,20 @@ public class WidgetPickerActivity extends BaseActivity implements
private static final String EXTRA_UI_SURFACE = "ui_surface";
private static final Pattern UI_SURFACE_PATTERN =
Pattern.compile("^(widgets|widgets_hub)$");
/**
* User ids that should be filtered out of the widget lists created by this activity.
*/
private static final String EXTRA_USER_ID_FILTER = "filtered_user_ids";
private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
private WidgetsModel mModel;
private LauncherAppState mApp;
private StringCache mStringCache;
private WidgetPredictionsRequester mWidgetPredictionsRequester;
private WidgetPickerDataProvider mWidgetPickerDataProvider;
private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
private int mDesiredWidgetWidth;
private int mDesiredWidgetHeight;
private WidgetCategoryFilter mWidgetCategoryInclusionFilter;
private WidgetCategoryFilter mWidgetCategoryExclusionFilter;
private int mWidgetCategoryFilter;
@Nullable
private String mUiSurface;
// Widgets existing on the host surface.
@NonNull
private List<AppWidgetProviderInfo> mAddedWidgets = new ArrayList<>();
@Nullable
private String mTitle;
@Nullable
private String mDescription;
/** A set of user ids that should be filtered out from the selected widgets. */
@NonNull
Set<Integer> mFilteredUserIds = new HashSet<>();
@Nullable
private WidgetsFullSheet mWidgetSheet;
private final Predicate<WidgetItem> mNoShortcutsFilter = widget -> {
final WidgetAcceptabilityVerdict verdict =
isWidgetAcceptable(widget, /* applySizeFilter=*/ false);
verdict.maybeLogVerdict();
return verdict.isAcceptable;
};
private final Predicate<WidgetItem> mHostSizeAndNoShortcutsFilter = widget -> {
final WidgetAcceptabilityVerdict verdict =
isWidgetAcceptable(widget, /* applySizeFilter=*/ true);
verdict.maybeLogVerdict();
return verdict.isAcceptable;
};
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -166,8 +110,7 @@ public class WidgetPickerActivity extends BaseActivity implements
mApp = LauncherAppState.getInstance(this);
InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
mDeviceProfile = idp.getDeviceProfile(this);
mModel = new WidgetsModel(mApp.getContext());
mWidgetPickerDataProvider = new WidgetPickerDataProvider(this);
mModel = new WidgetsModel();
setContentView(R.layout.widget_picker_activity);
mDragLayer = findViewById(R.id.drag_layer);
@@ -176,23 +119,15 @@ public class WidgetPickerActivity extends BaseActivity implements
WindowInsetsController wc = mDragLayer.getWindowInsetsController();
wc.hide(navigationBars() + statusBars());
BaseWidgetSheet widgetSheet = WidgetsFullSheet.show(this, true);
widgetSheet.disableNavBarScrim(true);
widgetSheet.addOnCloseListener(this::finish);
parseIntentExtras();
refreshAndBindWidgets();
}
@Override
protected void registerBackDispatcher() {
if (Utilities.ATLEAST_T) {
getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
new BackAnimationCallback());
}
}
private void parseIntentExtras() {
mTitle = getIntent().getStringExtra(EXTRA_PICKER_TITLE);
mDescription = getIntent().getStringExtra(EXTRA_PICKER_DESCRIPTION);
// 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 =
@@ -201,19 +136,8 @@ public class WidgetPickerActivity extends BaseActivity implements
getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_HEIGHT, 0);
// Defaults to '0' to indicate that there isn't a category filter.
// Negative value indicates it's an exclusion filter (e.g. NOT_KEYGUARD_CATEGORY.inv())
// Positive value indicates it's inclusion filter (e.g. HOME_SCREEN or KEYGUARD)
// Note: A filter can either be inclusion or exclusion filter; not both.
int inclusionFilter = getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
if (inclusionFilter < 0) {
Log.w(TAG, "Invalid EXTRA_CATEGORY_FILTER: " + inclusionFilter);
}
mWidgetCategoryInclusionFilter = new WidgetCategoryFilter(max(0, inclusionFilter));
int exclusionFilter = getIntent().getIntExtra(EXTRA_CATEGORY_EXCLUSION_FILTER, 0);
if (exclusionFilter > 0) {
Log.w(TAG, "Invalid EXTRA_CATEGORY_EXCLUSION_FILTER: " + exclusionFilter);
}
mWidgetCategoryExclusionFilter = new WidgetCategoryFilter(min(0 , exclusionFilter));
mWidgetCategoryFilter =
getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
String uiSurfaceParam = getIntent().getStringExtra(EXTRA_UI_SURFACE);
if (uiSurfaceParam != null && UI_SURFACE_PATTERN.matcher(uiSurfaceParam).matches()) {
@@ -226,18 +150,12 @@ public class WidgetPickerActivity extends BaseActivity implements
mAddedWidgets = addedWidgets;
}
}
ArrayList<Integer> filteredUsers = getIntent().getIntegerArrayListExtra(
EXTRA_USER_ID_FILTER);
mFilteredUserIds.clear();
if (filteredUsers != null) {
mFilteredUserIds.addAll(filteredUsers);
}
}
@NonNull
@Override
public WidgetPickerDataProvider getWidgetPickerDataProvider() {
return mWidgetPickerDataProvider;
public PopupDataProvider getPopupDataProvider() {
return mPopupDataProvider;
}
@Override
@@ -307,213 +225,121 @@ public class WidgetPickerActivity extends BaseActivity implements
};
}
/**
* Updates the model with widgets, applies filters and launches the widgets sheet once
* widgets are available
*/
/** Updates the model with widgets and provides them after applying the provided filter. */
private void refreshAndBindWidgets() {
MODEL_EXECUTOR.execute(() -> {
mModel.update(null);
StringCache stringCache = new StringCache();
stringCache.loadStrings(this);
bindStringCache(stringCache);
bindWidgets(mModel.getWidgetsByPackageItemForPicker());
// Open sheet once widgets are available, so that it doesn't interrupt the open
// animation.
openWidgetsSheet();
LauncherAppState app = LauncherAppState.getInstance(this);
mModel.update(app, null);
final List<WidgetsListBaseEntry> allWidgets =
mModel.getFilteredWidgetsListForPicker(
app.getContext(),
/*widgetItemFilter=*/ widget -> {
final WidgetAcceptabilityVerdict verdict =
isWidgetAcceptable(widget);
verdict.maybeLogVerdict();
return verdict.isAcceptable;
}
);
bindWidgets(allWidgets);
if (mUiSurface != null) {
mWidgetPredictionsRequester = new WidgetPredictionsRequester(
getApplicationContext(), mUiSurface,
mModel.getWidgetsByComponentKeyForPicker());
mWidgetPredictionsRequester.request(mAddedWidgets, this);
Map<PackageUserKey, List<WidgetItem>> 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 bindStringCache(final StringCache stringCache) {
MAIN_EXECUTOR.execute(() -> mStringCache = stringCache);
private void bindWidgets(List<WidgetsListBaseEntry> widgets) {
MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets));
}
private void bindWidgets(Map<PackageItemInfo, List<WidgetItem>> widgets) {
WidgetsListBaseEntriesBuilder builder = new WidgetsListBaseEntriesBuilder(
mApp.getContext());
final List<WidgetsListBaseEntry> allWidgets = builder.build(widgets, mNoShortcutsFilter);
// Default list is shown if host has additionally enforced size filtering.
@Nullable Predicate<WidgetItem> defaultListFilter =
hasHostSizeFilters() ? mHostSizeAndNoShortcutsFilter : null;
MAIN_EXECUTOR.execute(() -> {
mWidgetPickerDataProvider.setHostSpecifiedDefaultWidgetsFilter(defaultListFilter);
mWidgetPickerDataProvider.setWidgets(allWidgets);
});
}
private void openWidgetsSheet() {
MAIN_EXECUTOR.execute(() -> {
mWidgetSheet = WidgetsFullSheet.show(this, true);
mWidgetSheet.mayUpdateTitleAndDescription(mTitle, mDescription);
mWidgetSheet.disableNavBarScrim(true);
mWidgetSheet.addOnCloseListener(this::finish);
});
}
@Override
public void onPredictionsAvailable(List<ItemInfo> recommendedWidgets) {
// Bind recommendations once picker has finished open animation.
MAIN_EXECUTOR.getHandler().postDelayed(
() -> mWidgetPickerDataProvider.setWidgetRecommendations(recommendedWidgets),
mDeviceProfile.bottomSheetOpenDuration);
private void bindRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets));
}
@Override
protected void onDestroy() {
super.onDestroy();
mWidgetPickerDataProvider.destroy();
if (mWidgetPredictionsRequester != null) {
mWidgetPredictionsRequester.clear();
}
}
@Nullable
@Override
public StringCache getStringCache() {
return mStringCache;
}
/**
* Animation callback for different predictive back animation states for the widget picker.
*/
private class BackAnimationCallback extends FlingOnBackAnimationCallback {
@Nullable
OnBackAnimationCallback mActiveOnBackAnimationCallback;
@Override
public void onBackStartedCompat(@NonNull BackEvent backEvent) {
if (mActiveOnBackAnimationCallback != null) {
mActiveOnBackAnimationCallback.onBackCancelled();
}
if (mWidgetSheet != null) {
mActiveOnBackAnimationCallback = mWidgetSheet;
mActiveOnBackAnimationCallback.onBackStarted(backEvent);
}
}
@Override
public void onBackInvokedCompat() {
if (mActiveOnBackAnimationCallback == null) {
return;
}
mActiveOnBackAnimationCallback.onBackInvoked();
mActiveOnBackAnimationCallback = null;
}
@Override
public void onBackProgressedCompat(@NonNull BackEvent backEvent) {
if (mActiveOnBackAnimationCallback == null) {
return;
}
mActiveOnBackAnimationCallback.onBackProgressed(backEvent);
}
@Override
public void onBackCancelledCompat() {
if (mActiveOnBackAnimationCallback == null) {
return;
}
mActiveOnBackAnimationCallback.onBackCancelled();
mActiveOnBackAnimationCallback = null;
}
}
private boolean hasHostSizeFilters() {
// If optional filters such as size filter are present, we display them as default widgets.
return mDesiredWidgetWidth != 0 || mDesiredWidgetHeight != 0;
}
private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget,
boolean applySizeFilter) {
private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) {
final AppWidgetProviderInfo info = widget.widgetInfo;
if (info == null) {
return rejectWidget(widget, "shortcut");
}
if (mFilteredUserIds.contains(widget.user.getIdentifier())) {
if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) {
return rejectWidget(
widget,
"widget user: %d is being filtered",
widget.user.getIdentifier());
}
if (!mWidgetCategoryInclusionFilter.matches(info.widgetCategory)
|| !mWidgetCategoryExclusionFilter.matches(info.widgetCategory)) {
return rejectWidget(
widget,
"doesn't match category filter [inclusion=%d, exclusion=%d, widget=%d]",
mWidgetCategoryInclusionFilter.getCategoryMask(),
mWidgetCategoryExclusionFilter.getCategoryMask(),
"doesn't match category filter [filter=%d, widget=%d]",
mWidgetCategoryFilter,
info.widgetCategory);
}
if (applySizeFilter) {
if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
// Accept the widget if the desired dimensions are unspecified.
return acceptWidget(widget);
if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
// Accept the widget if the desired dimensions are unspecified.
return acceptWidget(widget);
}
final boolean isHorizontallyResizable =
(info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) {
if (info.maxResizeWidth > 0
&& info.maxResizeWidth >= info.minWidth
&& info.maxResizeWidth < mDesiredWidgetWidth) {
return rejectWidget(
widget,
"maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
info.maxResizeWidth,
mDesiredWidgetWidth);
}
final boolean isHorizontallyResizable =
(info.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
if (mDesiredWidgetWidth > 0 && isHorizontallyResizable) {
if (info.maxResizeWidth > 0
&& info.maxResizeWidth >= info.minWidth
&& info.maxResizeWidth < mDesiredWidgetWidth) {
return rejectWidget(
widget,
"maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
info.maxResizeWidth,
mDesiredWidgetWidth);
}
final int minWidth = Math.min(info.minResizeWidth, info.minWidth);
if (minWidth > mDesiredWidgetWidth) {
return rejectWidget(
widget,
"min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]",
info.minWidth,
info.minResizeWidth,
mDesiredWidgetWidth);
}
}
final int minWidth = min(info.minResizeWidth, info.minWidth);
if (minWidth > mDesiredWidgetWidth) {
return rejectWidget(
widget,
"min(minWidth[%d], minResizeWidth[%d]) > mDesiredWidgetWidth[%d]",
info.minWidth,
info.minResizeWidth,
mDesiredWidgetWidth);
}
final boolean isVerticallyResizable =
(info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
if (mDesiredWidgetHeight > 0 && isVerticallyResizable) {
if (info.maxResizeHeight > 0
&& info.maxResizeHeight >= info.minHeight
&& info.maxResizeHeight < mDesiredWidgetHeight) {
return rejectWidget(
widget,
"maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
info.maxResizeHeight,
mDesiredWidgetHeight);
}
final boolean isVerticallyResizable =
(info.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
if (mDesiredWidgetHeight > 0 && isVerticallyResizable) {
if (info.maxResizeHeight > 0
&& info.maxResizeHeight >= info.minHeight
&& info.maxResizeHeight < mDesiredWidgetHeight) {
return rejectWidget(
widget,
"maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
info.maxResizeHeight,
mDesiredWidgetHeight);
}
final int minHeight = min(info.minResizeHeight, info.minHeight);
if (minHeight > mDesiredWidgetHeight) {
return rejectWidget(
widget,
"min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]",
info.minHeight,
info.minResizeHeight,
mDesiredWidgetHeight);
}
final int minHeight = Math.min(info.minResizeHeight, info.minHeight);
if (minHeight > mDesiredWidgetHeight) {
return rejectWidget(
widget,
"min(minHeight[%d], minResizeHeight[%d]) > mDesiredWidgetHeight[%d]",
info.minHeight,
info.minResizeHeight,
mDesiredWidgetHeight);
}
}
if (!isHorizontallyResizable || !isVerticallyResizable) {
return rejectWidget(widget, "not resizeable");
}
if (!isHorizontallyResizable || !isVerticallyResizable) {
return rejectWidget(widget, "not resizeable");
}
return acceptWidget(widget);
@@ -31,7 +31,7 @@ import android.view.View;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.ColorInt;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.ContextCompat;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
@@ -84,9 +84,10 @@ public class AppsDividerView extends View implements FloatingHeaderRow {
getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height)
};
mStrokeColor = context.getColor(R.color.materialColorOutlineVariant);
mStrokeColor = ContextCompat.getColor(context, R.color.material_color_outline_variant);
mAllAppsLabelTextColor = context.getColor(R.color.materialColorOnSurfaceVariant);
mAllAppsLabelTextColor = ContextCompat.getColor(context,
R.color.material_color_on_surface_variant);
mAccessibilityManager = AccessibilityManager.getInstance(context);
setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(context));
@@ -253,9 +254,4 @@ public class AppsDividerView extends View implements FloatingHeaderRow {
public View getFocusedChild() {
return null;
}
@VisibleForTesting
public DividerType getDividerType() {
return mDividerType;
}
}
@@ -16,16 +16,12 @@
package com.android.launcher3.appprediction;
import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
@@ -35,6 +31,7 @@ import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.Flags;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.FloatingHeaderRow;
@@ -60,7 +57,7 @@ public class PredictionRowView<T extends Context & ActivityContext>
// Vertical padding of the icon that contributes to the expected cell height.
private final int mVerticalPadding;
// Extra padding that is used in the top app rows (prediction and search) that is not used in
// the regular A-Z list.
// the regular A-Z list. This only applies to single line label.
private final int mTopRowExtraHeight;
// Helper to drawing the focus indicator.
@@ -93,14 +90,6 @@ public class PredictionRowView<T extends Context & ActivityContext>
updateVisibility();
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
if (Build.VERSION.SDK_INT >= UPSIDE_DOWN_CAKE) {
info.setContainerTitle(mActivityContext.getString(R.string.title_app_suggestions));
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -149,8 +138,9 @@ public class PredictionRowView<T extends Context & ActivityContext>
int totalHeight = iconHeight + iconPadding + textHeight + mVerticalPadding * 2;
// Prediction row height will be 4dp bigger than the regular apps in A-Z list when two line
// is not enabled. Otherwise, the extra height will increase by just the textHeight.
int extraHeight = deviceProfile.inv.enableTwoLinesInAllApps
? (textHeight + mTopRowExtraHeight) : mTopRowExtraHeight;
int extraHeight = (Flags.enableTwolineToggle() &&
LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(getContext()))
? textHeight : mTopRowExtraHeight;
totalHeight += extraHeight;
return getVisibility() == GONE ? 0 : totalHeight + getPaddingTop() + getPaddingBottom();
}
@@ -300,9 +290,6 @@ public class PredictionRowView<T extends Context & ActivityContext>
writer.println(prefix + "\tmPredictionsEnabled: " + mPredictionsEnabled);
writer.println(prefix + "\tmPredictionUiUpdatePaused: " + mPredictionUiUpdatePaused);
writer.println(prefix + "\tmNumPredictedAppsPerRow: " + mNumPredictedAppsPerRow);
writer.println(prefix + "\tmPredictedApps: " + mPredictedApps.size());
for (WorkspaceItemInfo info : mPredictedApps) {
writer.println(prefix + "\t\t" + info);
}
writer.println(prefix + "\tmPredictedApps: " + mPredictedApps);
}
}
@@ -20,7 +20,6 @@ import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.IRemoteTransitionFinishedCallback
import android.window.RemoteTransition
import android.window.RemoteTransitionStub
@@ -30,12 +29,8 @@ import com.android.launcher3.statemanager.StateManager
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.quickstep.SystemUiProxy
import com.android.quickstep.TaskViewUtils
import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled
import com.android.quickstep.views.DesktopTaskView
import com.android.quickstep.views.TaskContainer
import com.android.quickstep.views.TaskView
import com.android.window.flags.Flags
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
import java.util.function.Consumer
/** Manage recents related operations with desktop tasks */
@@ -43,63 +38,42 @@ class DesktopRecentsTransitionController(
private val stateManager: StateManager<*, *>,
private val systemUiProxy: SystemUiProxy,
private val appThread: IApplicationThread,
private val depthController: DepthController?,
private val depthController: DepthController?
) {
/** Launch desktop tasks from recents view */
fun launchDesktopFromRecents(
desktopTaskView: DesktopTaskView,
animated: Boolean,
callback: Consumer<Boolean>? = null,
callback: Consumer<Boolean>? = null
) {
val animRunner =
RemoteDesktopLaunchTransitionRunner(
desktopTaskView,
animated,
stateManager,
depthController,
callback,
callback
)
val transition = RemoteTransition(animRunner, appThread, "RecentsToDesktop")
if (areMultiDesksFlagsEnabled()) {
systemUiProxy.activateDesk(desktopTaskView.deskId, transition)
} else {
systemUiProxy.showDesktopApps(desktopTaskView.displayId, transition)
}
systemUiProxy.showDesktopApps(desktopTaskView.display.displayId, transition)
}
/** Launch desktop tasks from recents view */
fun moveToDesktop(
taskContainer: TaskContainer,
transitionSource: DesktopModeTransitionSource,
successCallback: Runnable,
) {
systemUiProxy.moveToDesktop(
taskContainer.task.key.id,
transitionSource,
/* transition = */ null,
successCallback,
)
}
/** Move task to external display from recents view */
fun moveToExternalDisplay(taskId: Int) {
systemUiProxy.moveToExternalDisplay(taskId)
fun moveToDesktop(taskId: Int, transitionSource: DesktopModeTransitionSource) {
systemUiProxy.moveToDesktop(taskId, transitionSource)
}
private class RemoteDesktopLaunchTransitionRunner(
private val taskView: TaskView,
private val animated: Boolean,
private val desktopTaskView: DesktopTaskView,
private val stateManager: StateManager<*, *>,
private val depthController: DepthController?,
private val successCallback: Consumer<Boolean>?,
private val successCallback: Consumer<Boolean>?
) : RemoteTransitionStub() {
override fun startAnimation(
token: IBinder,
info: TransitionInfo,
t: SurfaceControl.Transaction,
finishCallback: IRemoteTransitionFinishedCallback,
finishCallback: IRemoteTransitionFinishedCallback
) {
val errorHandlingFinishCallback = Runnable {
try {
@@ -109,44 +83,16 @@ class DesktopRecentsTransitionController(
}
}
if (Flags.enableDesktopWindowingPersistence()) {
handleAnimationAfterReboot(info)
}
MAIN_EXECUTOR.execute {
val animator =
TaskViewUtils.composeRecentsDesktopLaunchAnimator(
taskView,
stateManager,
depthController,
info,
t,
) {
errorHandlingFinishCallback.run()
successCallback?.accept(true)
}
if (!animated) {
animator.setDuration(0)
}
animator.start()
}
}
/**
* Upon reboot the start bounds of a task is set to fullscreen with the recents transition.
* Check this case and set the start bounds to the end bounds so that the window doesn't
* jump from start bounds to end bounds during the animation. Tasks in desktop cannot
* normally have top bound as 0 due to status bar so this is a good indicator to identify
* reboot case.
*/
private fun handleAnimationAfterReboot(info: TransitionInfo) {
info.changes.forEach { change ->
if (
change.mode == TRANSIT_TO_FRONT &&
change.taskInfo?.isFreeform == true &&
change.startAbsBounds.top == 0 &&
change.startAbsBounds.left == 0
TaskViewUtils.composeRecentsDesktopLaunchAnimator(
desktopTaskView,
stateManager,
depthController,
info,
t
) {
change.setStartAbsBounds(change.endAbsBounds)
errorHandlingFinishCallback.run()
successCallback?.accept(true)
}
}
}
@@ -229,7 +229,9 @@ public class HotseatPredictionController implements DragController.DragListener,
(WorkspaceItemInfo) mPredictedItems.get(predictionIndex++);
if (isPredictedIcon(child) && child.isEnabled()) {
PredictedAppIcon icon = (PredictedAppIcon) child;
if (icon.applyFromWorkspaceItemWithAnimation(predictedItem, numViewsAnimated)) {
boolean animateIconChange = icon.shouldAnimateIconChange(predictedItem);
icon.applyFromWorkspaceItem(predictedItem, animateIconChange, numViewsAnimated);
if (animateIconChange) {
numViewsAnimated++;
}
icon.finishBinding(mPredictionLongClickListener);
@@ -534,9 +536,6 @@ public class HotseatPredictionController implements DragController.DragListener,
writer.println(prefix + "HotseatPredictionController");
writer.println(prefix + "\tFlags: " + getStateString(mPauseFlags));
writer.println(prefix + "\tmHotSeatItemsCount: " + mHotSeatItemsCount);
writer.println(prefix + "\tmPredictedItems: " + mPredictedItems.size());
for (ItemInfo info : mPredictedItems) {
writer.println(prefix + "\t\t" + info);
}
writer.println(prefix + "\tmPredictedItems: " + mPredictedItems);
}
}
@@ -17,6 +17,7 @@ package com.android.launcher3.hybridhotseat;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
import static com.android.launcher3.model.PredictionHelper.getAppTargetFromItemInfo;
import static com.android.launcher3.model.PredictionHelper.isTrackedForHotseatPrediction;
import static com.android.launcher3.model.PredictionHelper.wrapAppTargetWithItemLocation;
import android.app.prediction.AppTarget;
@@ -26,12 +27,9 @@ import android.os.Bundle;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.PredictionHelper;
import com.android.launcher3.model.data.ItemInfo;
import java.util.ArrayList;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Model helper for app predictions in workspace
@@ -45,18 +43,13 @@ public class HotseatPredictionModel {
*/
public static Bundle convertDataModelToAppTargetBundle(Context context, BgDataModel dataModel) {
Bundle bundle = new Bundle();
ArrayList<AppTargetEvent> events = dataModel.itemsIdMap
.stream()
.filter(PredictionHelper::isTrackedForHotseatPrediction)
.map(item -> {
AppTarget target = getAppTargetFromItemInfo(context, item);
return target != null
? wrapAppTargetWithItemLocation(target, AppTargetEvent.ACTION_PIN, item)
: null;
})
.filter(Objects::nonNull)
.collect(Collectors.toCollection(ArrayList::new));
ArrayList<AppTargetEvent> events = new ArrayList<>();
ArrayList<ItemInfo> workspaceItems = dataModel.getAllWorkspaceItems();
for (ItemInfo item : workspaceItems) {
AppTarget target = getAppTargetFromItemInfo(context, item);
if (target != null && !isTrackedForHotseatPrediction(item)) continue;
events.add(wrapAppTargetWithItemLocation(target, AppTargetEvent.ACTION_PIN, item));
}
ArrayList<AppTarget> currentTargets = new ArrayList<>();
FixedContainerItems hotseatItems = dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
if (hotseatItems != null) {
@@ -18,7 +18,6 @@ package com.android.launcher3.model;
import static com.android.launcher3.EncryptionType.ENCRYPTED;
import static com.android.launcher3.LauncherPrefs.nonRestorableItem;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG;
import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
import android.app.prediction.AppTarget;
@@ -32,9 +31,9 @@ import android.os.UserHandle;
import androidx.annotation.NonNull;
import com.android.launcher3.ConstantItem;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel.ModelUpdateTask;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
import com.android.launcher3.model.data.AppInfo;
@@ -65,8 +64,8 @@ public class PredictionUpdateTask implements ModelUpdateTask {
@Override
public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel,
@NonNull AllAppsList apps) {
IconCache iconCache = taskController.getIconCache();
Context context = taskController.getContext();
LauncherAppState app = taskController.getApp();
Context context = app.getContext();
// TODO: remove this
LauncherPrefs.get(context).put(LAST_PREDICTION_ENABLED, !mTargets.isEmpty());
@@ -84,7 +83,7 @@ public class PredictionUpdateTask implements ModelUpdateTask {
if (si != null) {
usersForChangedShortcuts.add(si.getUserHandle());
itemInfo = new WorkspaceItemInfo(si, context);
iconCache.getShortcutIcon(itemInfo, si);
app.getIconCache().getShortcutIcon(itemInfo, si);
} else {
String className = target.getClassName();
if (COMPONENT_CLASS_MARKER.equals(className)) {
@@ -96,7 +95,7 @@ public class PredictionUpdateTask implements ModelUpdateTask {
itemInfo = apps.data.stream()
.filter(info -> user.equals(info.user) && cn.equals(info.componentName))
.map(ai -> {
iconCache.getTitleAndIcon(ai, mPredictorState.lookupFlag);
app.getIconCache().getTitleAndIcon(ai, false);
return ai.makeWorkspaceItem(context);
})
.findAny()
@@ -107,7 +106,7 @@ public class PredictionUpdateTask implements ModelUpdateTask {
return null;
}
AppInfo ai = new AppInfo(context, lai, user);
iconCache.getTitleAndIcon(ai, lai, DEFAULT_LOOKUP_FLAG);
app.getIconCache().getTitleAndIcon(ai, lai, false);
return ai.makeWorkspaceItem(context);
});
@@ -123,7 +122,8 @@ public class PredictionUpdateTask implements ModelUpdateTask {
FixedContainerItems fci = new FixedContainerItems(mPredictorState.containerId, items);
dataModel.extraItems.put(fci.containerId, fci);
taskController.bindExtraContainerItems(fci);
usersForChangedShortcuts.forEach(u -> dataModel.updateShortcutPinnedState(context, u));
usersForChangedShortcuts.forEach(
u -> dataModel.updateShortcutPinnedState(app.getContext(), u));
// Save to disk
mPredictorState.storage.write(context, fci.items);
@@ -23,11 +23,9 @@ import static com.android.launcher3.LauncherPrefs.nonRestorableItem;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
import static com.android.launcher3.LauncherSettings.Favorites.DESKTOP_ICON_FLAG;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
import static com.android.launcher3.hybridhotseat.HotseatPredictionModel.convertDataModelToAppTargetBundle;
import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG;
import static com.android.launcher3.model.PredictionHelper.getAppTargetFromItemInfo;
import static com.android.launcher3.model.PredictionHelper.wrapAppTargetWithItemLocation;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -49,12 +47,12 @@ import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.util.StatsEvent;
import androidx.annotation.AnyThread;
import androidx.annotation.CallSuper;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -65,8 +63,7 @@ import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.Utilities;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.dagger.ApplicationContext;
import com.android.launcher3.icons.cache.CacheLookupFlag;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.logging.InstanceId;
import com.android.launcher3.logging.InstanceIdSequence;
@@ -84,7 +81,6 @@ import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.PersistedItemArray;
import com.android.quickstep.logging.SettingsChangeLogger;
import com.android.quickstep.logging.StatsLogCompatManager;
import com.android.quickstep.util.ContextualSearchStateManager;
import com.android.systemui.shared.system.SysUiStatsLog;
import java.util.ArrayList;
@@ -94,9 +90,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.stream.IntStream;
import javax.inject.Inject;
import javax.inject.Named;
import app.lawnchair.LawnchairApp;
import app.lawnchair.compat.LawnchairQuickstepCompat;
@@ -115,39 +108,30 @@ public class QuickstepModelDelegate extends ModelDelegate {
nonRestorableItem("LAST_SNAPSHOT_TIME_MILLIS", 0L, ENCRYPTED);
@VisibleForTesting
final PredictorState mAllAppsState = new PredictorState(
CONTAINER_PREDICTION, "all_apps_predictions", DEFAULT_LOOKUP_FLAG);
final PredictorState mAllAppsState = new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions");
@VisibleForTesting
final PredictorState mHotseatState = new PredictorState(
CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions", DESKTOP_ICON_FLAG);
final PredictorState mHotseatState = new PredictorState(CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions");
@VisibleForTesting
final PredictorState mWidgetsRecommendationState = new PredictorState(
CONTAINER_WIDGETS_PREDICTION, "widgets_prediction", DESKTOP_ICON_FLAG);
final PredictorState mWidgetsRecommendationState = new PredictorState(CONTAINER_WIDGETS_PREDICTION,
"widgets_prediction");
private final InvariantDeviceProfile mIDP;
private final PackageManagerHelper mPmHelper;
private final AppEventProducer mAppEventProducer;
private final StatsManager mStatsManager;
private StatsManager mStatsManager;
protected boolean mActive = false;
@Inject
public QuickstepModelDelegate(@ApplicationContext Context context,
InvariantDeviceProfile idp,
PackageManagerHelper pmHelper,
@Nullable @Named("ICONS_DB") String dbFileName) {
public QuickstepModelDelegate(Context context) {
super(context);
mIDP = idp;
mPmHelper = pmHelper;
mAppEventProducer = new AppEventProducer(context, this::onAppTargetEvent);
StatsLogCompatManager.LOGS_CONSUMER.add(mAppEventProducer);
// Only register for launcher snapshot logging if this is the primary ModelDelegate
// instance, as there will be additional instances that may be destroyed at any time.
mStatsManager = TextUtils.isEmpty(dbFileName)
? null : context.getSystemService(StatsManager.class);
mIDP = InvariantDeviceProfile.INSTANCE.get(context);
StatsLogCompatManager.LOGS_CONSUMER.add(mAppEventProducer);
try {
mStatsManager = context.getSystemService(StatsManager.class);
} catch (Throwable e) {
Log.e(TAG, "Failed to get StatsManager", e);
}
}
@CallSuper
@@ -176,10 +160,13 @@ public class QuickstepModelDelegate extends ModelDelegate {
// TODO: Implement caching and preloading
WorkspaceItemFactory factory =
new WorkspaceItemFactory(mContext, ums, mPmHelper, pinnedShortcuts, numColumns,
state.containerId, state.lookupFlag);
new WorkspaceItemFactory(mApp, ums, mPmHelper, pinnedShortcuts, numColumns,
state.containerId);
FixedContainerItems fci = new FixedContainerItems(state.containerId,
state.storage.read(mContext, factory, ums.allUsers::get));
state.storage.read(mApp.getContext(), factory, ums.allUsers::get));
if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
bindPredictionItems(callbacks, fci);
}
mDataModel.extraItems.put(state.containerId, fci);
}
@@ -189,7 +176,8 @@ public class QuickstepModelDelegate extends ModelDelegate {
FixedContainerItems widgetPredictionFCI = new FixedContainerItems(
mWidgetsRecommendationState.containerId, new ArrayList<>());
// Widgets prediction isn't used frequently. And thus, it is not persisted on disk.
// Widgets prediction isn't used frequently. And thus, it is not persisted on
// disk.
mDataModel.extraItems.put(mWidgetsRecommendationState.containerId, widgetPredictionFCI);
bindPredictionItems(callbacks, widgetPredictionFCI);
@@ -206,6 +194,7 @@ public class QuickstepModelDelegate extends ModelDelegate {
});
}
@CallSuper
@Override
@WorkerThread
public void bindAllModelExtras(@NonNull BgDataModel.Callbacks[] callbacks) {
@@ -227,12 +216,9 @@ public class QuickstepModelDelegate extends ModelDelegate {
mActive = true;
}
@WorkerThread
@Override
public void workspaceLoadComplete() {
super.workspaceLoadComplete();
// Initialize ContextualSearchStateManager.
ContextualSearchStateManager.INSTANCE.get(mContext);
recreatePredictors();
}
@@ -242,7 +228,7 @@ public class QuickstepModelDelegate extends ModelDelegate {
super.modelLoadComplete();
// Log snapshot of the model
LauncherPrefs prefs = LauncherPrefs.get(mContext);
LauncherPrefs prefs = LauncherPrefs.get(mApp.getContext());
long lastSnapshotTimeMillis = prefs.get(LAST_SNAPSHOT_TIME_MILLIS);
// Log snapshot only if previous snapshot was older than a day
long now = System.currentTimeMillis();
@@ -261,23 +247,32 @@ public class QuickstepModelDelegate extends ModelDelegate {
InstanceId instanceId = new InstanceIdSequence().newInstanceId();
for (ItemInfo info : itemsIdMap) {
CollectionInfo parent = getContainer(info, itemsIdMap);
StatsLogCompatManager.writeSnapshot(info.buildProto(parent, mContext), instanceId);
StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId);
}
additionalSnapshotEvents(instanceId);
prefs.put(LAST_SNAPSHOT_TIME_MILLIS, now);
}
registerSnapshotLoggingCallback();
// Only register for launcher snapshot logging if this is the primary
// ModelDelegate
// instance, as there will be additional instances that may be destroyed at any
// time.
if (mIsPrimaryInstance && LawnchairApp.isRecentsEnabled()) {
registerSnapshotLoggingCallback();
}
}
protected void additionalSnapshotEvents(InstanceId snapshotInstanceId){}
protected void additionalSnapshotEvents(InstanceId snapshotInstanceId) {
}
/**
* Registers a callback to log launcher workspace layout using Statsd pulled atom.
* Registers a callback to log launcher workspace layout using Statsd pulled
* atom.
*/
private void registerSnapshotLoggingCallback() {
protected void registerSnapshotLoggingCallback() {
if (mStatsManager == null || !LawnchairQuickstepCompat.ATLEAST_R) {
Log.d(TAG, "Skipping snapshot logging");
Log.d(TAG, "Failed to get StatsManager");
return;
}
try {
@@ -294,7 +289,7 @@ public class QuickstepModelDelegate extends ModelDelegate {
for (ItemInfo info : itemsIdMap) {
CollectionInfo parent = getContainer(info, itemsIdMap);
LauncherAtom.ItemInfo itemInfo = info.buildProto(parent, mContext);
LauncherAtom.ItemInfo itemInfo = info.buildProto(parent);
Log.d(TAG, itemInfo.toString());
StatsEvent statsEvent = StatsLogCompatManager.buildStatsEvent(itemInfo,
instanceId);
@@ -307,8 +302,7 @@ public class QuickstepModelDelegate extends ModelDelegate {
additionalSnapshotEvents(instanceId);
SettingsChangeLogger.INSTANCE.get(mContext).logSnapshot(instanceId);
return StatsManager.PULL_SUCCESS;
}
);
});
Log.d(TAG, "Successfully registered for launcher snapshot logging!");
} catch (Throwable e) {
Log.e(TAG, "Failed to register launcher snapshot logging callback with StatsManager",
@@ -344,13 +338,12 @@ public class QuickstepModelDelegate extends ModelDelegate {
}
}
@WorkerThread
@Override
public void destroy() {
super.destroy();
mActive = false;
StatsLogCompatManager.LOGS_CONSUMER.remove(mAppEventProducer);
if (mStatsManager != null && LawnchairQuickstepCompat.ATLEAST_R) {
if (mIsPrimaryInstance && mStatsManager != null && LawnchairQuickstepCompat.ATLEAST_R) {
try {
mStatsManager.clearPullAtomCallback(SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT);
} catch (Throwable e) {
@@ -372,28 +365,29 @@ public class QuickstepModelDelegate extends ModelDelegate {
if (!Utilities.ATLEAST_Q || !mActive) {
return;
}
AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
Context context = mApp.getContext();
AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
if (apm == null) {
return;
}
int usagePerm = mContext.checkCallingOrSelfPermission(Manifest.permission.PACKAGE_USAGE_STATS);
int usagePerm = mApp.getContext().checkCallingOrSelfPermission(Manifest.permission.PACKAGE_USAGE_STATS);
if (usagePerm != PackageManager.PERMISSION_GRANTED)
return;
registerPredictor(mAllAppsState, apm.createAppPredictionSession(
new AppPredictionContext.Builder(mContext)
new AppPredictionContext.Builder(context)
.setUiSurface("home")
.setPredictedTargetCount(mIDP.numDatabaseAllAppsColumns)
.build()));
// TODO: get bundle
registerHotseatPredictor(apm, mContext);
registerHotseatPredictor(apm, context);
registerWidgetsPredictor(apm.createAppPredictionSession(
new AppPredictionContext.Builder(mContext)
new AppPredictionContext.Builder(context)
.setUiSurface("widgets")
.setExtras(getBundleForWidgetsOnWorkspace(mContext, mDataModel))
.setExtras(getBundleForWidgetsOnWorkspace(context, mDataModel))
.setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION)
.build()));
}
@@ -404,11 +398,12 @@ public class QuickstepModelDelegate extends ModelDelegate {
if (!mActive) {
return;
}
AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
Context context = mApp.getContext();
AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
if (apm == null) {
return;
}
registerHotseatPredictor(apm, mContext);
registerHotseatPredictor(apm, context);
}
private void registerHotseatPredictor(AppPredictionManager apm, Context context) {
@@ -424,7 +419,13 @@ public class QuickstepModelDelegate extends ModelDelegate {
state.setTargets(Collections.emptyList());
state.predictor = predictor;
state.predictor.registerPredictionUpdates(
MODEL_EXECUTOR, t -> handleUpdate(state, t));
MODEL_EXECUTOR, new AppPredictor.Callback() {
@Keep
@Override
public void onTargetsAvailable(@NonNull List<AppTarget> targets) {
handleUpdate(state, targets);
}
});
state.predictor.requestPredictionUpdate();
}
@@ -433,19 +434,23 @@ public class QuickstepModelDelegate extends ModelDelegate {
// No diff, skip
return;
}
mModel.enqueueModelUpdateTask(new PredictionUpdateTask(state, targets));
mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets));
}
private void registerWidgetsPredictor(AppPredictor predictor) {
mWidgetsRecommendationState.predictor = predictor;
mWidgetsRecommendationState.predictor.registerPredictionUpdates(
MODEL_EXECUTOR, targets -> {
if (mWidgetsRecommendationState.setTargets(targets)) {
// No diff, skip
return;
MODEL_EXECUTOR, new AppPredictor.Callback() {
@Keep
@Override
public void onTargetsAvailable(@NonNull List<AppTarget> targets) {
if (mWidgetsRecommendationState.setTargets(targets)) {
// No diff, skip
return;
}
mApp.getModel().enqueueModelUpdateTask(
new WidgetsPredictionUpdateTask(mWidgetsRecommendationState, targets));
}
mModel.enqueueModelUpdateTask(
new WidgetsPredictionUpdateTask(mWidgetsRecommendationState, targets));
});
mWidgetsRecommendationState.predictor.requestPredictionUpdate();
}
@@ -453,7 +458,7 @@ public class QuickstepModelDelegate extends ModelDelegate {
@VisibleForTesting
void onAppTargetEvent(AppTargetEvent event, int client) {
PredictorState state;
switch(client) {
switch (client) {
case CONTAINER_PREDICTION:
state = mAllAppsState;
break;
@@ -481,17 +486,17 @@ public class QuickstepModelDelegate extends ModelDelegate {
private Bundle getBundleForWidgetsOnWorkspace(Context context, BgDataModel dataModel) {
Bundle bundle = new Bundle();
ArrayList<AppTargetEvent> widgetEvents =
dataModel.itemsIdMap.stream()
.filter(PredictionHelper::isTrackedForWidgetPrediction)
.map(item -> {
AppTarget target = getAppTargetFromItemInfo(context, item);
if (target == null) return null;
return wrapAppTargetWithItemLocation(
target, AppTargetEvent.ACTION_PIN, item);
})
.filter(Objects::nonNull)
.collect(toCollection(ArrayList::new));
ArrayList<AppTargetEvent> widgetEvents = dataModel.getAllWorkspaceItems().stream()
.filter(PredictionHelper::isTrackedForWidgetPrediction)
.map(item -> {
AppTarget target = getAppTargetFromItemInfo(context, item);
if (target == null)
return null;
return wrapAppTargetWithItemLocation(
target, AppTargetEvent.ACTION_PIN, item);
})
.filter(Objects::nonNull)
.collect(toCollection(ArrayList::new));
bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, widgetEvents);
return bundle;
}
@@ -501,15 +506,13 @@ public class QuickstepModelDelegate extends ModelDelegate {
public final int containerId;
public final PersistedItemArray<ItemInfo> storage;
public AppPredictor predictor;
public CacheLookupFlag lookupFlag;
private List<AppTarget> mLastTargets;
PredictorState(int containerId, String storageName, CacheLookupFlag lookupFlag) {
PredictorState(int containerId, String storageName) {
this.containerId = containerId;
storage = new PersistedItemArray<>(storageName);
mLastTargets = Collections.emptyList();
this.lookupFlag = lookupFlag;
}
public void destroyPredictor() {
@@ -556,27 +559,24 @@ public class QuickstepModelDelegate extends ModelDelegate {
private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory<ItemInfo> {
private final Context mContext;
private final LauncherAppState mAppState;
private final UserManagerState mUMS;
private final PackageManagerHelper mPmHelper;
private final Map<ShortcutKey, ShortcutInfo> mPinnedShortcuts;
private final int mMaxCount;
private final int mContainer;
private final CacheLookupFlag mLookupFlag;
private int mReadCount = 0;
protected WorkspaceItemFactory(
Context context, UserManagerState ums,
protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums,
PackageManagerHelper pmHelper, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts,
int maxCount, int container, CacheLookupFlag lookupFlag) {
mContext = context;
int maxCount, int container) {
mAppState = appState;
mUMS = ums;
mPmHelper = pmHelper;
mPinnedShortcuts = pinnedShortcuts;
mMaxCount = maxCount;
mContainer = container;
mLookupFlag = lookupFlag;
}
@Nullable
@@ -587,7 +587,7 @@ public class QuickstepModelDelegate extends ModelDelegate {
}
switch (itemType) {
case ITEM_TYPE_APPLICATION: {
LauncherActivityInfo lai = mContext
LauncherActivityInfo lai = mAppState.getContext()
.getSystemService(LauncherApps.class)
.resolveActivity(intent, user);
if (lai == null) {
@@ -595,15 +595,14 @@ public class QuickstepModelDelegate extends ModelDelegate {
}
AppInfo info = new AppInfo(
lai,
UserCache.INSTANCE.get(mContext).getUserInfo(user),
ApiWrapper.INSTANCE.get(mContext),
UserCache.INSTANCE.get(mAppState.getContext()).getUserInfo(user),
ApiWrapper.INSTANCE.get(mAppState.getContext()),
mPmHelper,
mUMS.isUserQuiet(user));
info.container = mContainer;
LauncherAppState.getInstance(mContext).getIconCache()
.getTitleAndIcon(info, lai, mLookupFlag);
mAppState.getIconCache().getTitleAndIcon(info, lai, false);
mReadCount++;
return info.makeWorkspaceItem(mContext);
return info.makeWorkspaceItem(mAppState.getContext());
}
case ITEM_TYPE_DEEP_SHORTCUT: {
ShortcutKey key = ShortcutKey.fromIntent(intent, user);
@@ -614,9 +613,9 @@ public class QuickstepModelDelegate extends ModelDelegate {
if (si == null) {
return null;
}
WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mContext);
WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mAppState.getContext());
wii.container = mContainer;
LauncherAppState.getInstance(mContext).getIconCache().getShortcutIcon(wii, si);
mAppState.getIconCache().getShortcutIcon(wii, si);
mReadCount++;
return wii;
}
@@ -44,30 +44,23 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.android.launcher3.R;
import com.android.launcher3.dagger.ApplicationContext;
import com.android.launcher3.dagger.LauncherAppSingleton;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.popup.RemoteActionShortcut;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.util.DaggerSingletonObject;
import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.launcher3.views.ActivityContext;
import com.android.quickstep.dagger.QuickstepBaseAppComponent;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
/**
* Data model for digital wellbeing status of apps.
*/
@LauncherAppSingleton
public final class WellbeingModel implements SafeCloseable {
private static final String TAG = "WellbeingModel";
private static final int[] RETRY_TIMES_MS = {5000, 15000, 30000};
@@ -82,16 +75,18 @@ public final class WellbeingModel implements SafeCloseable {
private static final String EXTRA_PACKAGES = "packages";
private static final String EXTRA_SUCCESS = "success";
public static final DaggerSingletonObject<WellbeingModel> INSTANCE =
new DaggerSingletonObject<>(QuickstepBaseAppComponent::getWellbeingModel);
public static final MainThreadInitializedObject<WellbeingModel> INSTANCE =
new MainThreadInitializedObject<>(WellbeingModel::new);
private final Context mContext;
private final String mWellbeingProviderPkg;
private final Handler mWorkerHandler;
private final ContentObserver mContentObserver;
private final SimpleBroadcastReceiver mWellbeingAppChangeReceiver;
private final SimpleBroadcastReceiver mAppAddRemoveReceiver;
private final SimpleBroadcastReceiver mWellbeingAppChangeReceiver =
new SimpleBroadcastReceiver(t -> restartObserver());
private final SimpleBroadcastReceiver mAppAddRemoveReceiver =
new SimpleBroadcastReceiver(this::onAppPackageChanged);
private final Object mModelLock = new Object();
// Maps the action Id to the corresponding RemoteAction
@@ -100,19 +95,12 @@ public final class WellbeingModel implements SafeCloseable {
private boolean mIsInTest;
@Inject
WellbeingModel(@ApplicationContext final Context context,
DaggerSingletonTracker tracker) {
private WellbeingModel(final Context context) {
mContext = context;
mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg);
mWorkerHandler = new Handler(TextUtils.isEmpty(mWellbeingProviderPkg)
? Executors.UI_HELPER_EXECUTOR.getLooper()
: Executors.getPackageExecutor(mWellbeingProviderPkg).getLooper());
mWellbeingAppChangeReceiver =
new SimpleBroadcastReceiver(context, mWorkerHandler, t -> restartObserver());
mAppAddRemoveReceiver =
new SimpleBroadcastReceiver(context, mWorkerHandler, this::onAppPackageChanged);
mContentObserver = new ContentObserver(mWorkerHandler) {
@Override
@@ -121,10 +109,8 @@ public final class WellbeingModel implements SafeCloseable {
}
};
mWorkerHandler.post(this::initializeInBackground);
tracker.addCloseable(this);
}
@WorkerThread
private void initializeInBackground() {
if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
mContext.registerReceiver(
@@ -148,8 +134,8 @@ public final class WellbeingModel implements SafeCloseable {
public void close() {
if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
mWorkerHandler.post(() -> {
mWellbeingAppChangeReceiver.unregisterReceiverSafely();
mAppAddRemoveReceiver.unregisterReceiverSafely();
mWellbeingAppChangeReceiver.unregisterReceiverSafely(mContext);
mAppAddRemoveReceiver.unregisterReceiverSafely(mContext);
mContext.getContentResolver().unregisterContentObserver(mContentObserver);
});
}
@@ -16,6 +16,7 @@
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;
@@ -39,6 +40,7 @@ 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;
@@ -47,6 +49,7 @@ 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;
@@ -54,91 +57,72 @@ 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 implements AppPredictor.Callback {
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";
// container/screenid/[positionx,positiony]/[spanx,spany]
// Matches the format passed used by PredictionHelper; But, position and size values aren't
// used, so, we pass default values.
@VisibleForTesting
static final String LAUNCH_LOCATION = "workspace/1/[0,0]/[2,2]";
@Nullable
private AppPredictor mAppPredictor;
private final Context mContext;
@NonNull
private final String mUiSurface;
private boolean mPredictionsAvailable;
@Nullable
private WidgetPredictionsListener mPredictionsListener = null;
@Nullable Predicate<WidgetItem> mFilter = null;
@NonNull
private final Map<ComponentKey, WidgetItem> mAllWidgets;
private final Map<PackageUserKey, List<WidgetItem>> mAllWidgets;
public WidgetPredictionsRequester(Context context, @NonNull String uiSurface,
@NonNull Map<ComponentKey, WidgetItem> allWidgets) {
@NonNull Map<PackageUserKey, List<WidgetItem>> allWidgets) {
mContext = context;
mUiSurface = uiSurface;
mAllWidgets = Collections.unmodifiableMap(allWidgets);
}
// AppPredictor.Callback -> onTargetsAvailable
@Override
@WorkerThread
public void onTargetsAvailable(List<AppTarget> targets) {
List<WidgetItem> filteredPredictions = filterPredictions(targets, mAllWidgets, mFilter);
List<ItemInfo> mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions);
if (!mPredictionsAvailable && mPredictionsListener != null) {
mPredictionsAvailable = true;
MAIN_EXECUTOR.execute(
() -> mPredictionsListener.onPredictionsAvailable(mappedPredictions));
}
}
/**
* Requests one time predictions from the app predictions manager and invokes provided callback
* once predictions are available. Any previous requests may be cancelled.
* 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 listener consumer of prediction results to be called when predictions are
* available; any previous listener will no longer receive updates.
* @param callback consumer of prediction results to be called when predictions are
* available
*/
@WorkerThread // e.g. MODEL_EXECUTOR
public void request(List<AppWidgetProviderInfo> existingWidgets,
WidgetPredictionsListener listener) {
clear();
mPredictionsListener = listener;
mFilter = notOnUiSurfaceFilter(existingWidgets);
Consumer<List<ItemInfo>> callback) {
Bundle bundle = buildBundleForPredictionSession(existingWidgets, mUiSurface);
Predicate<WidgetItem> filter = notOnUiSurfaceFilter(existingWidgets);
AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
if (apm == null) {
return;
}
MODEL_EXECUTOR.execute(() -> {
clear();
AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
if (apm == null) {
return;
}
Bundle bundle = buildBundleForPredictionSession(existingWidgets);
mAppPredictor = apm.createAppPredictionSession(
new AppPredictionContext.Builder(mContext)
.setUiSurface(mUiSurface)
.setExtras(bundle)
.setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION)
.build());
mAppPredictor.registerPredictionUpdates(MODEL_EXECUTOR, /*callback=*/ this);
mAppPredictor.requestPredictionUpdate();
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<AppWidgetProviderInfo> addedWidgets) {
static Bundle buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets,
String uiSurface) {
Bundle bundle = new Bundle();
ArrayList<AppTargetEvent> addedAppTargetEvents = new ArrayList<>();
for (AppWidgetProviderInfo info : addedWidgets) {
ComponentName componentName = info.provider;
AppTargetEvent appTargetEvent = buildAppTargetEvent(info, componentName);
AppTargetEvent appTargetEvent = buildAppTargetEvent(uiSurface, info, componentName);
addedAppTargetEvents.add(appTargetEvent);
}
bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, addedAppTargetEvents);
@@ -150,13 +134,13 @@ public class WidgetPredictionsRequester implements AppPredictor.Callback {
* predictor.
* Also see {@link PredictionHelper}
*/
private static AppTargetEvent buildAppTargetEvent(AppWidgetProviderInfo info,
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(
LAUNCH_LOCATION).build();
return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN)
.setLaunchLocation(uiSurface).build();
}
/**
@@ -172,26 +156,49 @@ public class WidgetPredictionsRequester implements AppPredictor.Callback {
return widgetItem -> !existingComponentKeys.contains(widgetItem);
}
/** Provides the predictions returned by the predictor to the registered callback. */
@WorkerThread
private void bindPredictions(List<AppTarget> targets, Predicate<WidgetItem> filter,
Consumer<List<ItemInfo>> callback) {
List<WidgetItem> filteredPredictions = filterPredictions(targets, mAllWidgets, filter);
List<ItemInfo> 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<WidgetItem> filterPredictions(List<AppTarget> predictions,
@NonNull Map<ComponentKey, WidgetItem> allWidgets,
@Nullable Predicate<WidgetItem> filter) {
Map<PackageUserKey, List<WidgetItem>> allWidgets, Predicate<WidgetItem> filter) {
List<WidgetItem> servicePredictedItems = new ArrayList<>();
List<WidgetItem> localFilteredWidgets = new ArrayList<>();
for (AppTarget prediction : predictions) {
List<WidgetItem> widgetsInPackage = allWidgets.get(
new PackageUserKey(prediction.getPackageName(), prediction.getUser()));
if (widgetsInPackage == null || widgetsInPackage.isEmpty()) {
continue;
}
String className = prediction.getClassName();
if (!TextUtils.isEmpty(className)) {
WidgetItem widgetItem = allWidgets.get(
new ComponentKey(new ComponentName(prediction.getPackageName(), className),
prediction.getUser()));
if (widgetItem != null && (filter == null || filter.test(widgetItem))) {
servicePredictedItems.add(widgetItem);
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;
@@ -201,34 +208,26 @@ public class WidgetPredictionsRequester implements AppPredictor.Callback {
* Converts the list of {@link WidgetItem}s to the list of {@link ItemInfo}s.
*/
private List<ItemInfo> mapWidgetItemsToItemInfo(List<WidgetItem> widgetItems) {
WidgetRecommendationCategoryProvider categoryProvider =
new WidgetRecommendationCategoryProvider();
return widgetItems.stream()
.map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
categoryProvider.getWidgetRecommendationCategory(mContext, it)))
.collect(Collectors.toList());
List<ItemInfo> 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.unregisterPredictionUpdates(this);
mAppPredictor.destroy();
mAppPredictor = null;
}
mPredictionsListener = null;
mPredictionsAvailable = false;
mFilter = null;
}
/**
* Listener class to listen to updates from the {@link WidgetPredictionsRequester}
*/
public interface WidgetPredictionsListener {
/**
* Callback method that is called when the predicted widgets are available.
* @param predictions list of predicted widgets {@link PendingAddWidgetInfo}
*/
void onPredictionsAvailable(List<ItemInfo> predictions);
}
}
@@ -15,12 +15,8 @@
*/
package com.android.launcher3.model;
import static com.android.launcher3.Flags.enableTieredWidgetsByDefaultInPicker;
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
import static com.android.launcher3.model.ModelUtils.WIDGET_FILTER;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toMap;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
@@ -30,7 +26,6 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.android.launcher3.LauncherModel.ModelUpdateTask;
import com.android.launcher3.R;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
import com.android.launcher3.model.data.ItemInfo;
@@ -39,11 +34,10 @@ 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.Random;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/** Task to update model as a result of predicted widgets update */
@@ -66,74 +60,45 @@ public final class WidgetsPredictionUpdateTask implements ModelUpdateTask {
@Override
public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel,
@NonNull AllAppsList apps) {
Set<ComponentKey> widgetsInWorkspace = dataModel.itemsIdMap
.stream()
.filter(WIDGET_FILTER)
.map(item -> new ComponentKey(item.getTargetComponent(), item.user))
.collect(Collectors.toSet());
// Widgets (excluding shortcuts & already added widgets) that belong to apps eligible for
// being in predictions.
Map<ComponentKey, WidgetItem> allEligibleWidgets =
dataModel.widgetsModel.getWidgetsByComponentKeyForPicker()
.entrySet()
.stream()
.filter(entry -> entry.getValue().widgetInfo != null
&& !widgetsInWorkspace.contains(entry.getValue())
).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
Context context = taskController.getContext();
Set<ComponentKey> widgetsInWorkspace = dataModel.appWidgets.stream().map(
widget -> new ComponentKey(widget.providerName, widget.user)).collect(
Collectors.toSet());
Predicate<WidgetItem> notOnWorkspace = w -> !widgetsInWorkspace.contains(w);
Map<ComponentKey, WidgetItem> allWidgets =
dataModel.widgetsModel.getAllWidgetComponentsWithoutShortcuts();
List<WidgetItem> servicePredictedItems = new ArrayList<>();
List<String> addedWidgetApps = new ArrayList<>();
for (AppTarget app : mTargets) {
ComponentKey componentKey = new ComponentKey(
new ComponentName(app.getPackageName(), app.getClassName()), app.getUser());
WidgetItem widget = allEligibleWidgets.get(componentKey);
if (widget == null) { // widget not eligible.
WidgetItem widget = allWidgets.get(componentKey);
if (widget == null) {
continue;
}
String className = app.getClassName();
if (!TextUtils.isEmpty(className)) {
servicePredictedItems.add(widget);
addedWidgetApps.add(componentKey.componentName.getPackageName());
if (notOnWorkspace.test(widget)) {
servicePredictedItems.add(widget);
}
}
}
int minPredictionCount = context.getResources().getInteger(
R.integer.widget_predictions_min_count);
if (enableTieredWidgetsByDefaultInPicker()
&& servicePredictedItems.size() < minPredictionCount) {
// Eligible apps that aren't already part of predictions.
Map<String, List<WidgetItem>> eligibleWidgetsByApp =
allEligibleWidgets.values().stream()
.filter(w -> !addedWidgetApps.contains(
w.componentName.getPackageName()))
.collect(groupingBy(w -> w.componentName.getPackageName()));
// Randomize available apps list
List<String> appPackages = new ArrayList<>(eligibleWidgetsByApp.keySet());
Collections.shuffle(appPackages);
int widgetsToAdd = minPredictionCount - servicePredictedItems.size();
for (String appPackage : appPackages) {
if (widgetsToAdd <= 0) break;
List<WidgetItem> widgetsForApp = eligibleWidgetsByApp.get(appPackage);
int index = new Random().nextInt(widgetsForApp.size());
// Add a random widget from the app.
servicePredictedItems.add(widgetsForApp.get(index));
widgetsToAdd--;
}
List<ItemInfo> items;
if (enableCategorizedWidgetSuggestions()) {
Context context = taskController.getApp().getContext();
WidgetRecommendationCategoryProvider categoryProvider =
WidgetRecommendationCategoryProvider.newInstance(context);
items = servicePredictedItems.stream()
.map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
categoryProvider.getWidgetRecommendationCategory(context, it)))
.collect(Collectors.toList());
} else {
items = servicePredictedItems.stream()
.map(it -> new PendingAddWidgetInfo(it.widgetInfo,
CONTAINER_WIDGETS_PREDICTION)).collect(
Collectors.toList());
}
WidgetRecommendationCategoryProvider categoryProvider =
new WidgetRecommendationCategoryProvider();
List<ItemInfo> items = servicePredictedItems.stream()
.map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
categoryProvider.getWidgetRecommendationCategory(context, it)))
.collect(Collectors.toList());
FixedContainerItems fixedContainerItems =
new FixedContainerItems(mPredictorState.containerId, items);
@@ -61,7 +61,6 @@ public class ProxyActivityStarter extends Activity {
}
} catch (NullPointerException | ActivityNotFoundException | SecurityException
| SendIntentException e) {
Log.w(TAG, "Proxy activity starter could not start activity: ", e);
mParams.deliverResult(this, RESULT_CANCELED, null);
}
finishAndRemoveTask();
@@ -19,7 +19,6 @@ package com.android.launcher3.statehandlers;
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
import android.animation.Animator;
@@ -30,8 +29,6 @@ import android.view.View;
import android.view.ViewRootImpl;
import android.view.ViewTreeObserver;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
@@ -40,11 +37,11 @@ import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.quickstep.util.BaseDepthController;
import com.patrykmichalik.opto.core.PreferenceExtensionsKt;
import java.io.PrintWriter;
import java.util.function.Consumer;
import com.patrykmichalik.opto.core.PreferenceExtensionsKt;
import app.lawnchair.compat.LawnchairQuickstepCompat;
import app.lawnchair.preferences2.PreferenceManager2;
@@ -53,8 +50,8 @@ import app.lawnchair.preferences2.PreferenceManager2;
*/
public class DepthController extends BaseDepthController implements StateHandler<LauncherState>,
BaseActivity.MultiWindowModeChangedListener {
@VisibleForTesting
final ViewTreeObserver.OnDrawListener mOnDrawListener = this::onLauncherDraw;
private final ViewTreeObserver.OnDrawListener mOnDrawListener = this::onLauncherDraw;
private final Consumer<Boolean> mCrossWindowBlurListener = this::setCrossWindowBlursEnabled;
@@ -65,10 +62,6 @@ public class DepthController extends BaseDepthController implements StateHandler
private View.OnAttachStateChangeListener mOnAttachListener;
// Ensure {@link mOnDrawListener} is added only once to avoid spamming DragLayer's mRunQueue
// via {@link View#post(Runnable)}
private boolean mIsOnDrawListenerAdded = false;
private final boolean mEnableDepth;
public DepthController(Launcher l) {
@@ -82,46 +75,41 @@ public class DepthController extends BaseDepthController implements StateHandler
ViewRootImpl viewRootImpl = view.getViewRootImpl();
try {
if (Utilities.ATLEAST_Q) {
setBaseSurface(viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null);
setSurface(viewRootImpl != null ? viewRootImpl.getSurfaceControl() : null);
}
} catch (Throwable t) {
// LC-Ignored
// Ignore any exceptions
}
view.post(this::removeOnDrawListener);
view.post(() -> view.getViewTreeObserver().removeOnDrawListener(mOnDrawListener));
}
private void ensureDependencies() {
View rootView = mLauncher.getRootView();
if (rootView == null) {
return;
}
if (mOnAttachListener != null) {
return;
}
mOnAttachListener = new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
try {
UI_HELPER_EXECUTOR.execute(() ->
CrossWindowBlurListeners.getInstance().addListener(
mLauncher.getMainExecutor(), mCrossWindowBlurListener));
} catch (Throwable t) {
// LC-Ignored
if (mLauncher.getRootView() != null && mOnAttachListener == null) {
View rootView = mLauncher.getRootView();
mOnAttachListener = new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
try {
CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(),
mCrossWindowBlurListener);
} catch (Throwable t) {
// Ignore
}
mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener);
// To handle the case where window token is invalid during last setDepth call.
applyDepthAndBlur();
}
mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener);
// To handle the case where window token is invalid during last setDepth call.
applyDepthAndBlur();
@Override
public void onViewDetachedFromWindow(View view) {
removeSecondaryListeners();
}
};
rootView.addOnAttachStateChangeListener(mOnAttachListener);
if (rootView.isAttachedToWindow()) {
mOnAttachListener.onViewAttachedToWindow(rootView);
}
@Override
public void onViewDetachedFromWindow(View view) {
removeSecondaryListeners();
}
};
rootView.addOnAttachStateChangeListener(mOnAttachListener);
if (rootView.isAttachedToWindow()) {
mOnAttachListener.onViewAttachedToWindow(rootView);
}
}
@@ -138,12 +126,12 @@ public class DepthController extends BaseDepthController implements StateHandler
}
private void removeSecondaryListeners() {
try {
UI_HELPER_EXECUTOR.execute(() ->
CrossWindowBlurListeners.getInstance()
.removeListener(mCrossWindowBlurListener));
} catch (Throwable t) {
// LC-Ignored
if (mCrossWindowBlurListener != null) {
try {
CrossWindowBlurListeners.getInstance().removeListener(mCrossWindowBlurListener);
} catch (Throwable t) {
// Ignore
}
}
if (mOpaquenessListener != null) {
mLauncher.getScrimView().removeOpaquenessListener(mOpaquenessListener);
@@ -155,10 +143,10 @@ public class DepthController extends BaseDepthController implements StateHandler
*/
public void setActivityStarted(boolean isStarted) {
if (isStarted) {
addOnDrawListener();
mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener);
} else {
removeOnDrawListener();
setBaseSurface(null);
mLauncher.getDragLayer().getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
setSurface(null);
}
}
@@ -170,13 +158,13 @@ public class DepthController extends BaseDepthController implements StateHandler
stateDepth.setValue(toState.getDepth(mLauncher));
if (toState == LauncherState.BACKGROUND_APP) {
addOnDrawListener();
mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener);
}
}
@Override
public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
PendingAnimation animation) {
PendingAnimation animation) {
if (config.hasAnimationFlag(SKIP_DEPTH_CONTROLLER)
|| mIgnoreStateChangesDuringMultiWindowAnimation) {
return;
@@ -195,30 +183,14 @@ public class DepthController extends BaseDepthController implements StateHandler
super.applyDepthAndBlur();
}
} catch (Throwable t) {
// LC-Ignored
// Ignore
}
}
@Override
protected void onInvalidSurface() {
// Lets wait for surface to become valid again
addOnDrawListener();
}
private void addOnDrawListener() {
if (mIsOnDrawListenerAdded) {
return;
}
mLauncher.getDragLayer().getViewTreeObserver().addOnDrawListener(mOnDrawListener);
mIsOnDrawListenerAdded = true;
}
private void removeOnDrawListener() {
if (!mIsOnDrawListenerAdded) {
return;
}
mLauncher.getDragLayer().getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
mIsOnDrawListenerAdded = false;
}
@Override
@@ -226,7 +198,7 @@ public class DepthController extends BaseDepthController implements StateHandler
mIgnoreStateChangesDuringMultiWindowAnimation = true;
ObjectAnimator mwAnimation = ObjectAnimator.ofFloat(stateDepth, MULTI_PROPERTY_VALUE,
mLauncher.getStateManager().getState().getDepth(mLauncher, isInMultiWindowMode))
mLauncher.getStateManager().getState().getDepth(mLauncher, isInMultiWindowMode))
.setDuration(300);
mwAnimation.addListener(new AnimatorListenerAdapter() {
@Override
@@ -242,8 +214,7 @@ public class DepthController extends BaseDepthController implements StateHandler
writer.println(prefix + "DepthController");
writer.println(prefix + "\tmMaxBlurRadius=" + mMaxBlurRadius);
writer.println(prefix + "\tmCrossWindowBlursEnabled=" + mCrossWindowBlursEnabled);
writer.println(prefix + "\tmBaseSurface=" + mBaseSurface);
writer.println(prefix + "\tmBaseSurfaceOverride=" + mBaseSurfaceOverride);
writer.println(prefix + "\tmSurface=" + mSurface);
writer.println(prefix + "\tmStateDepth=" + stateDepth.getValue());
writer.println(prefix + "\tmWidgetDepth=" + widgetDepth.getValue());
writer.println(prefix + "\tmCurrentBlur=" + mCurrentBlur);
@@ -253,4 +224,4 @@ public class DepthController extends BaseDepthController implements StateHandler
writer.println(prefix + "\tmPauseBlurs=" + mPauseBlurs);
writer.println(prefix + "\tmWaitingOnSurfaceValidity=" + mWaitingOnSurfaceValidity);
}
}
}
@@ -16,82 +16,36 @@
package com.android.launcher3.taskbar;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.os.UserHandle;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.util.BaseContext;
import com.android.launcher3.util.NavigationMode;
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.util.Themes;
import com.android.quickstep.SystemUiProxy;
import com.android.launcher3.views.ActivityContext;
import java.util.ArrayList;
import java.util.List;
// TODO(b/218912746): Share more behavior to avoid all apps context depending directly on taskbar.
/** Base for common behavior between taskbar window contexts. */
public abstract class BaseTaskbarContext extends BaseContext
implements SystemShortcut.BubbleActivityStarter {
public abstract class BaseTaskbarContext extends ContextThemeWrapper implements ActivityContext {
protected final LayoutInflater mLayoutInflater;
private final List<OnDeviceProfileChangeListener> mDPChangeListeners = new ArrayList<>();
public BaseTaskbarContext(Context windowContext, boolean isPrimaryDisplay) {
public BaseTaskbarContext(Context windowContext) {
super(windowContext, Themes.getActivityThemeRes(windowContext));
mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
}
/**
* Returns whether taskbar is transient or persistent. External displays will be persistent.
*
* @return {@code true} if transient, {@code false} if persistent.
*/
public abstract boolean isTransientTaskbar();
/**
* Returns whether the taskbar is pinned in gesture navigation mode.
*/
public abstract boolean isPinnedTaskbar();
/**
* Returns the current navigation mode. External displays will be in THREE_BUTTONS mode.
*/
public abstract NavigationMode getNavigationMode();
/**
* Returns whether the taskbar is in desktop mode.
*/
public abstract boolean isInDesktopMode();
/**
* Returns whether the taskbar is forced to be pinned when home is visible.
*/
public abstract boolean showLockedTaskbarOnHome();
/**
* Returns whether desktop taskbar (pinned taskbar that shows desktop tasks) is to be used on
* the display because the display is a freeform display.
*/
public abstract boolean showDesktopTaskbarForFreeformDisplay();
/**
* Returns whether the taskbar is displayed on primary or external display.
*/
public abstract boolean isPrimaryDisplay();
@Override
public final LayoutInflater getLayoutInflater() {
return mLayoutInflater;
}
@Override
public void showShortcutBubble(ShortcutInfo info) {
if (info == null) return;
SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info);
}
@Override
public void showAppBubble(Intent intent, UserHandle user) {
if (intent == null || intent.getPackage() == null) return;
SystemUiProxy.INSTANCE.get(this).showAppBubble(intent, user);
public final List<OnDeviceProfileChangeListener> getOnDeviceProfileChangeListeners() {
return mDPChangeListeners;
}
/** Callback invoked when a drag is initiated within this context. */
@@ -25,27 +25,20 @@ import androidx.annotation.Nullable;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.statemanager.StatefulContainer;
import com.android.quickstep.FallbackActivityInterface;
import com.android.quickstep.GestureState;
import com.android.quickstep.RecentsAnimationCallbacks;
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.fallback.RecentsState;
import com.android.quickstep.util.TISBindHelper;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.RecentsViewContainer;
import java.io.PrintWriter;
import java.util.stream.Stream;
/**
* A data source which integrates with the fallback RecentsActivity instance (for 3P launchers).
* @param <T> The type of the RecentsViewContainer that will handle Recents state changes.
*/
public class FallbackTaskbarUIController
<T extends RecentsViewContainer & StatefulContainer<RecentsState>>
extends TaskbarUIController {
public class FallbackTaskbarUIController extends TaskbarUIController {
private final T mRecentsContainer;
private final RecentsActivity mRecentsActivity;
private final StateManager.StateListener<RecentsState> mStateListener =
new StateManager.StateListener<RecentsState>() {
@@ -53,12 +46,8 @@ public class FallbackTaskbarUIController
public void onStateTransitionStart(RecentsState toState) {
animateToRecentsState(toState);
RecentsView recentsView = getRecentsView();
if (recentsView == null) {
return;
}
// Handle tapping on live tile.
recentsView.setTaskLaunchListener(toState == RecentsState.DEFAULT
getRecentsView().setTaskLaunchListener(toState == RecentsState.DEFAULT
? (() -> animateToRecentsState(RecentsState.BACKGROUND_APP)) : null);
}
@@ -74,41 +63,30 @@ public class FallbackTaskbarUIController
}
};
public FallbackTaskbarUIController(T recentsContainer) {
mRecentsContainer = recentsContainer;
public FallbackTaskbarUIController(RecentsActivity recentsActivity) {
mRecentsActivity = recentsActivity;
}
@Override
protected void init(TaskbarControllers taskbarControllers) {
super.init(taskbarControllers);
mRecentsContainer.setTaskbarUIController(this);
mRecentsContainer.getStateManager().addStateListener(mStateListener);
mRecentsActivity.setTaskbarUIController(this);
mRecentsActivity.getStateManager().addStateListener(mStateListener);
}
@Override
protected void onDestroy() {
super.onDestroy();
RecentsView recentsView = getRecentsView();
if (recentsView != null) {
recentsView.setTaskLaunchListener(null);
}
mRecentsContainer.setTaskbarUIController(null);
mRecentsContainer.getStateManager().removeStateListener(mStateListener);
}
@Nullable
@Override
public Animator getParallelAnimationToGestureEndTarget(GestureState.GestureEndTarget endTarget,
long duration, RecentsAnimationCallbacks callbacks) {
return createAnimToRecentsState(
FallbackActivityInterface.INSTANCE.stateFromGestureEndTarget(endTarget), duration);
mRecentsActivity.setTaskbarUIController(null);
mRecentsActivity.getStateManager().removeStateListener(mStateListener);
}
/**
* Creates an animation to animate the taskbar for the given state (but does not start it).
* Currently this animation just force stashes the taskbar in Overview.
*/
private Animator createAnimToRecentsState(RecentsState toState, long duration) {
public Animator createAnimToRecentsState(RecentsState toState, long duration) {
// Force stash taskbar (disallow unstashing) when:
// - in a 3P launcher or overview task.
// - not running in a test harness (unstash is needed for tests)
@@ -130,8 +108,8 @@ public class FallbackTaskbarUIController
}
@Override
public @Nullable RecentsView getRecentsView() {
return mRecentsContainer.getOverviewPanel();
public RecentsView getRecentsView() {
return mRecentsActivity.getOverviewPanel();
}
@Override
@@ -146,21 +124,18 @@ public class FallbackTaskbarUIController
private boolean isIn3pHomeOrRecents() {
TopTaskTracker.CachedTaskInfo topTask = TopTaskTracker.INSTANCE
.get(mControllers.taskbarActivityContext).getCachedTopTask(true,
mRecentsContainer.asContext().getDisplayId());
.get(mControllers.taskbarActivityContext).getCachedTopTask(true);
return topTask.isHomeTask() || topTask.isRecentsTask();
}
@Nullable
@Override
protected TISBindHelper getTISBindHelper() {
return mRecentsActivity.getTISBindHelper();
}
@Override
protected String getTaskbarUIControllerName() {
return "FallbackTaskbarUIController<" + mRecentsContainer.getClass().getSimpleName() + ">";
}
@Override
protected void dumpLogs(String prefix, PrintWriter pw) {
super.dumpLogs(prefix, pw);
pw.println(String.format("%s\tRecentsState=%s", prefix,
mRecentsContainer.getStateManager().getState()));
return "FallbackTaskbarUIController";
}
}
@@ -15,28 +15,20 @@
*/
package com.android.launcher3.taskbar;
import static com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.Flags;
import com.android.launcher3.R;
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
import com.android.launcher3.util.TouchController;
import com.android.quickstep.RecentsFilterState;
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LayoutUtils;
import com.android.quickstep.util.SingleTask;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -45,7 +37,6 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -53,14 +44,12 @@ import java.util.stream.Collectors;
* Handles initialization of the {@link KeyboardQuickSwitchViewController}.
*/
public final class KeyboardQuickSwitchController implements
TaskbarControllers.LoggableTaskbarController, TouchController {
TaskbarControllers.LoggableTaskbarController {
@VisibleForTesting
public static final int MAX_TASKS = 6;
@NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks();
// Callback used to notify when the KQS view is closed.
@Nullable private Runnable mOnClosed;
// Initialized on init
@Nullable private RecentsModel mModel;
@@ -70,21 +59,12 @@ public final class KeyboardQuickSwitchController implements
private int mTaskListChangeId = -1;
// Only empty before the recent tasks list has been loaded the first time
@NonNull private List<GroupTask> mTasks = new ArrayList<>();
// Set of task IDs filtered out of tasks in recents model to generate list of tasks to show in
// the Keyboard Quick Switch view. Non empty only if the view has been shown in response to
// toggling taskbar overflow button.
@NonNull private Set<Integer> mExcludedTaskIds = Collections.emptySet();
private int mNumHiddenTasks = 0;
// Initialized in init
private TaskbarControllers mControllers;
@Nullable private KeyboardQuickSwitchViewController mQuickSwitchViewController;
@Nullable private TaskbarOverlayContext mOverlayContext;
private boolean mHasDesktopTask = false;
private boolean mWasDesktopTaskFilteredOut = false;
/** Initialize the controller. */
public void init(@NonNull TaskbarControllers controllers) {
@@ -102,12 +82,10 @@ public final class KeyboardQuickSwitchController implements
return;
}
int currentFocusedIndex = mQuickSwitchViewController.getCurrentFocusedIndex();
boolean wasOpenedFromTaskbar = mQuickSwitchViewController.wasOpenedFromTaskbar();
onDestroy();
if (currentFocusedIndex != -1) {
mControllers.taskbarActivityContext.getMainThreadHandler().post(
() -> openQuickSwitchView(currentFocusedIndex, mExcludedTaskIds,
wasOpenedFromTaskbar));
() -> openQuickSwitchView(currentFocusedIndex));
}
}
@@ -115,189 +93,76 @@ public final class KeyboardQuickSwitchController implements
openQuickSwitchView(-1);
}
/**
* Opens or closes the view in response to taskbar action. The view shows a filtered list of
* tasks.
* @param taskIdsToExclude A list of tasks to exclude in the opened view.
* @param onClosed A callback used to notify when the KQS view is closed.
*/
void toggleQuickSwitchViewForTaskbar(@NonNull Set<Integer> taskIdsToExclude,
@NonNull Runnable onClosed) {
mOnClosed = onClosed;
// Close the view if its shown, and was opened from the taskbar.
if (mQuickSwitchViewController != null
&& !mQuickSwitchViewController.isCloseAnimationRunning()
&& mQuickSwitchViewController.wasOpenedFromTaskbar()) {
closeQuickSwitchView(true);
return;
}
openQuickSwitchView(-1, taskIdsToExclude, true);
}
private void openQuickSwitchView(int currentFocusedIndex) {
openQuickSwitchView(currentFocusedIndex, Collections.emptySet(), false);
}
private void openQuickSwitchView(int currentFocusedIndex,
@NonNull Set<Integer> taskIdsToExclude,
boolean wasOpenedFromTaskbar) {
if (mQuickSwitchViewController != null) {
if (!mQuickSwitchViewController.isCloseAnimationRunning()) {
if (mQuickSwitchViewController.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) {
return;
}
// Relayout the KQS view instead of recreating a new one if it is the current
// trigger surface is different than the previous one.
final int currentFocusIndexOverride =
currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
? 0 : currentFocusedIndex;
// Skip the task reload if the list is not changed.
if (!mModel.isTaskListValid(mTaskListChangeId) || !taskIdsToExclude.equals(
mExcludedTaskIds)) {
final boolean shouldShowDesktopTasks = mControllers.taskbarDesktopModeController
.shouldShowDesktopTasksInTaskbar();
mExcludedTaskIds = taskIdsToExclude;
mTaskListChangeId = mModel.getTasks((tasks) -> {
processLoadedTasks(tasks, taskIdsToExclude);
mQuickSwitchViewController.updateQuickSwitchView(
mTasks,
wasOpenedFromTaskbar ? 0 : mNumHiddenTasks,
currentFocusIndexOverride,
mHasDesktopTask,
mWasDesktopTaskFilteredOut);
}, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER
: RecentsFilterState.getDesktopTaskFilter());
}
mQuickSwitchViewController.updateLayoutForSurface(wasOpenedFromTaskbar,
currentFocusIndexOverride);
return;
} else {
// Allow the KQS to be reopened during the close animation to make it more
// responsive.
closeQuickSwitchView(false);
}
// Allow the KQS to be reopened during the close animation to make it more responsive
closeQuickSwitchView(false);
}
mOverlayContext = mControllers.taskbarOverlayController.requestWindow();
if (Flags.taskbarOverflow()) {
mOverlayContext.getDragLayer().addTouchController(this);
}
TaskbarOverlayContext overlayContext =
mControllers.taskbarOverlayController.requestWindow();
KeyboardQuickSwitchView keyboardQuickSwitchView =
(KeyboardQuickSwitchView) mOverlayContext.getLayoutInflater()
(KeyboardQuickSwitchView) overlayContext.getLayoutInflater()
.inflate(
R.layout.keyboard_quick_switch_view,
mOverlayContext.getDragLayer(),
overlayContext.getDragLayer(),
/* attachToRoot= */ false);
mQuickSwitchViewController = new KeyboardQuickSwitchViewController(
mControllers, mOverlayContext, keyboardQuickSwitchView, mControllerCallbacks);
mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks);
final boolean shouldShowDesktopTasks = mControllers.taskbarDesktopModeController
.shouldShowDesktopTasksInTaskbar();
DesktopVisibilityController desktopController =
LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
final boolean onDesktop =
desktopController != null && desktopController.areDesktopTasksVisible();
if (mModel.isTaskListValid(mTaskListChangeId)
&& taskIdsToExclude.equals(mExcludedTaskIds)) {
if (mModel.isTaskListValid(mTaskListChangeId)) {
// When we are opening the KQS with no focus override, check if the first task is
// running. If not, focus that first task.
mQuickSwitchViewController.openQuickSwitchView(
mTasks,
wasOpenedFromTaskbar ? 0 : mNumHiddenTasks,
mNumHiddenTasks,
/* updateTasks= */ false,
currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
? 0 : currentFocusedIndex,
shouldShowDesktopTasks,
mHasDesktopTask,
mWasDesktopTaskFilteredOut,
wasOpenedFromTaskbar);
onDesktop);
return;
}
mExcludedTaskIds = taskIdsToExclude;
mTaskListChangeId = mModel.getTasks((tasks) -> {
processLoadedTasks(tasks, taskIdsToExclude);
if (onDesktop) {
processLoadedTasksOnDesktop(tasks);
} else {
processLoadedTasks(tasks);
}
// Check if the first task is running after the recents model has updated so that we use
// the correct index.
mQuickSwitchViewController.openQuickSwitchView(
mTasks,
wasOpenedFromTaskbar ? 0 : mNumHiddenTasks,
mNumHiddenTasks,
/* updateTasks= */ true,
currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
? 0 : currentFocusedIndex,
shouldShowDesktopTasks,
mHasDesktopTask,
mWasDesktopTaskFilteredOut,
wasOpenedFromTaskbar);
}, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER
: RecentsFilterState.getDesktopTaskFilter());
onDesktop);
});
}
private boolean shouldExcludeTask(GroupTask task, Set<Integer> taskIdsToExclude) {
return Flags.taskbarOverflow() && task.getTasks().stream().anyMatch(
t -> taskIdsToExclude.contains(t.key.id));
}
private void processLoadedTasks(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
mHasDesktopTask = false;
mWasDesktopTaskFilteredOut = false;
if (mControllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar()) {
processLoadedTasksOnDesktop(tasks, taskIdsToExclude);
} else {
processLoadedTasksOutsideDesktop(tasks, taskIdsToExclude);
}
}
private void processLoadedTasksOutsideDesktop(List<GroupTask> tasks,
Set<Integer> taskIdsToExclude) {
private void processLoadedTasks(List<GroupTask> tasks) {
// Only store MAX_TASK tasks, from most to least recent
Collections.reverse(tasks);
mTasks = tasks.stream()
.filter(task -> !(task instanceof DesktopTask)
&& !shouldExcludeTask(task, taskIdsToExclude))
.limit(MAX_TASKS)
.collect(Collectors.toList());
for (int i = 0; i < tasks.size(); i++) {
if (tasks.get(i) instanceof DesktopTask) {
mHasDesktopTask = true;
if (i < mTasks.size()) {
mWasDesktopTaskFilteredOut = true;
}
break;
}
}
mNumHiddenTasks = Math.max(0,
tasks.size() - (mWasDesktopTaskFilteredOut ? 1 : 0) - MAX_TASKS);
mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS);
}
private void processLoadedTasksOnDesktop(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
// Find all desktop tasks.
List<DesktopTask> desktopTasks = tasks.stream()
.filter(t -> t instanceof DesktopTask)
.map(t -> (DesktopTask) t)
.toList();
private void processLoadedTasksOnDesktop(List<GroupTask> tasks) {
// Find the single desktop task that contains a grouping of desktop tasks
DesktopTask desktopTask = findDesktopTask(tasks);
// Apps on the connected displays seem to be in different Desktop tasks even with the
// multiple desktops flag disabled. So, until multiple desktops is implemented the following
// should help with team-fooding Alt+tab on connected displays. Post multiple desktop,
// further changes maybe required to support launching selected desktops.
if (enableAltTabKqsOnConnectedDisplays()) {
mTasks = desktopTasks.stream()
.flatMap(t -> t.getTasks().stream())
.map(SingleTask::new)
.filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
.collect(Collectors.toList());
mNumHiddenTasks = Math.max(0, tasks.size() - desktopTasks.size());
} else if (!desktopTasks.isEmpty()) {
mTasks = desktopTasks.get(0).getTasks().stream()
.map(SingleTask::new)
.filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
.collect(Collectors.toList());
if (desktopTask != null) {
mTasks = desktopTask.tasks.stream().map(GroupTask::new).collect(Collectors.toList());
// All other tasks, apart from the grouped desktop task, are hidden
mNumHiddenTasks = Math.max(0, tasks.size() - 1);
} else {
@@ -307,6 +172,14 @@ public final class KeyboardQuickSwitchController implements
}
}
@Nullable
private DesktopTask findDesktopTask(List<GroupTask> tasks) {
return (DesktopTask) tasks.stream()
.filter(t -> t instanceof DesktopTask)
.findFirst()
.orElse(null);
}
void closeQuickSwitchView() {
closeQuickSwitchView(true);
}
@@ -328,54 +201,12 @@ public final class KeyboardQuickSwitchController implements
? -1 : mQuickSwitchViewController.launchFocusedTask();
}
@Override
public boolean onControllerTouchEvent(MotionEvent ev) {
return false;
}
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
if (mQuickSwitchViewController == null
|| mOverlayContext == null
|| !Flags.taskbarOverflow()) {
return false;
}
TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer();
if (ev.getAction() == MotionEvent.ACTION_DOWN
&& !mQuickSwitchViewController.isEventOverKeyboardQuickSwitch(dragLayer, ev)) {
closeQuickSwitchView(true);
}
return false;
}
void onDestroy() {
if (mQuickSwitchViewController != null) {
mQuickSwitchViewController.onDestroy();
}
}
@VisibleForTesting
boolean isShownFromTaskbar() {
return isShown() && mQuickSwitchViewController.wasOpenedFromTaskbar();
}
@VisibleForTesting
boolean isShown() {
return mQuickSwitchViewController != null
&& !mQuickSwitchViewController.isCloseAnimationRunning();
}
@VisibleForTesting
List<Integer> shownTaskIds() {
if (!isShown()) {
return Collections.emptyList();
}
return mTasks.stream().flatMap(
groupTask -> groupTask.getTasks().stream().map(task -> task.key.id)).toList();
}
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "KeyboardQuickSwitchController:");
@@ -383,16 +214,17 @@ public final class KeyboardQuickSwitchController implements
pw.println(prefix + "\tisOpen=" + (mQuickSwitchViewController != null));
pw.println(prefix + "\tmNumHiddenTasks=" + mNumHiddenTasks);
pw.println(prefix + "\tmTaskListChangeId=" + mTaskListChangeId);
pw.println(prefix + "\tmHasDesktopTask=" + mHasDesktopTask);
pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut);
pw.println(prefix + "\tmTasks=[");
for (GroupTask task : mTasks) {
int count = 0;
for (Task t : task.getTasks()) {
ComponentName cn = t.getTopComponent();
pw.println(prefix + "\t\tt" + (++count) + ": (id=" + t.key.id
+ "; package=" + (cn != null ? cn.getPackageName() + ")" : "no package)"));
}
Task task1 = task.task1;
Task task2 = task.task2;
ComponentName cn1 = task1.getTopComponent();
ComponentName cn2 = task2 != null ? task2.getTopComponent() : null;
pw.println(prefix + "\t\tt1: (id=" + task1.key.id
+ "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)")
+ " t2: (id=" + (task2 != null ? task2.key.id : "-1")
+ "; package=" + (cn2 != null ? cn2.getPackageName() + ")"
: "no package)"));
}
pw.println(prefix + "\t]");
@@ -403,41 +235,24 @@ public final class KeyboardQuickSwitchController implements
class ControllerCallbacks {
int getTaskCount() {
return mTasks.size() + (mNumHiddenTasks == 0 ? 0 : 1);
}
@Nullable
GroupTask getTaskAt(int index) {
return index < 0 || index >= mTasks.size() ? null : mTasks.get(index);
}
void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback) {
mModel.getThumbnailCache().getThumbnailInBackground(task,
thumbnailData -> {
task.thumbnail = thumbnailData;
callback.accept(thumbnailData);
});
mModel.getThumbnailCache().updateThumbnailInBackground(task, callback);
}
void updateIconInBackground(Task task, Consumer<Task> callback) {
mModel.getIconCache().getIconInBackground(task, (icon, contentDescription, title) -> {
task.icon = icon;
task.titleDescription = contentDescription;
task.title = title;
callback.accept(task);
});
}
void onCloseStarted() {
if (mOnClosed != null) {
mOnClosed.run();
mOnClosed = null;
}
mModel.getIconCache().updateIconInBackground(task, callback);
}
void onCloseComplete() {
if (Flags.taskbarOverflow() && mOverlayContext != null) {
mOverlayContext.getDragLayer()
.removeTouchController(KeyboardQuickSwitchController.this);
}
mOverlayContext = null;
mQuickSwitchViewController = null;
}
@@ -445,23 +260,15 @@ public final class KeyboardQuickSwitchController implements
if (task == null) {
return false;
}
ActivityManager.RunningTaskInfo runningTaskInfo =
ActivityManagerWrapper.getInstance().getRunningTask();
if (runningTaskInfo == null) {
return false;
}
int runningTaskId = ActivityManagerWrapper.getInstance().getRunningTask().taskId;
Task task2 = task.task2;
int runningTaskId = runningTaskInfo.taskId;
return task.containsTask(runningTaskId);
return runningTaskId == task.task1.key.id
|| (task2 != null && runningTaskId == task2.key.id);
}
boolean isFirstTaskRunning() {
return isTaskRunning(getTaskAt(0));
}
boolean isAspectRatioSquare() {
return mControllers != null && LayoutUtils.isAspectRatioSquare(
mControllers.taskbarActivityContext.getDeviceProfile().aspectRatio);
}
}
}
@@ -36,40 +36,37 @@ import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.launcher3.R;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
import com.android.quickstep.util.BorderAnimator;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.wm.shell.shared.TypefaceUtils;
import com.android.wm.shell.shared.TypefaceUtils.FontFamily;
import kotlin.Unit;
import java.util.function.Consumer;
import kotlin.Unit;
/**
* A view that displays a recent task during a keyboard quick switch.
*/
public class KeyboardQuickSwitchTaskView extends ConstraintLayout {
private static final float THUMBNAIL_BLUR_RADIUS = 1f;
private static final int INVALID_BORDER_RADIUS = -1;
@ColorInt private final int mBorderColor;
@ColorInt private final int mBorderRadius;
@ColorInt
private final int mBorderColor;
@Nullable private BorderAnimator mBorderAnimator;
@Nullable
private BorderAnimator mBorderAnimator;
@Nullable private ImageView mThumbnailView1;
@Nullable private ImageView mThumbnailView2;
@Nullable private ImageView mIcon1;
@Nullable private ImageView mIcon2;
@Nullable private View mContent;
// Describe the task position in the parent container. Used to add information about the task's
// position in a task list to the task view's content description.
private int mIndexInParent = -1;
private int mTotalTasksInParent = -1;
@Nullable
private ImageView mThumbnailView1;
@Nullable
private ImageView mThumbnailView2;
@Nullable
private ImageView mIcon1;
@Nullable
private ImageView mIcon2;
@Nullable
private View mContent;
public KeyboardQuickSwitchTaskView(@NonNull Context context) {
this(context, null);
@@ -97,8 +94,6 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout {
mBorderColor = ta.getColor(
R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR);
mBorderRadius = ta.getDimensionPixelSize(
R.styleable.TaskView_focusBorderRadius, INVALID_BORDER_RADIUS);
ta.recycle();
}
@@ -111,27 +106,14 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout {
mIcon2 = findViewById(R.id.icon_2);
mContent = findViewById(R.id.content);
Preconditions.assertNotNull(mContent);
TypefaceUtils.setTypeface(
mContent.findViewById(R.id.large_text),
FontFamily.GSF_HEADLINE_LARGE_EMPHASIZED
);
TypefaceUtils.setTypeface(
mContent.findViewById(R.id.small_text),
FontFamily.GSF_LABEL_LARGE
);
Resources resources = mContext.getResources();
Preconditions.assertNotNull(mContent);
mBorderAnimator = BorderAnimator.createScalingBorderAnimator(
/* borderRadiusPx= */ mBorderRadius != INVALID_BORDER_RADIUS
? mBorderRadius
: resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_task_view_radius),
/* borderRadiusPx= */ resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_task_view_radius),
/* borderWidthPx= */ resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_border_width),
/* borderStrokePx= */ resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_border_stroke),
/* boundsBuilder= */ bounds -> {
bounds.set(0, 0, getWidth(), getHeight());
return Unit.INSTANCE;
@@ -162,105 +144,36 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout {
applyThumbnail(mThumbnailView1, task1, thumbnailUpdateFunction);
applyThumbnail(mThumbnailView2, task2, thumbnailUpdateFunction);
// Update content description, even in cases task icons, and content descriptions need to be
// loaded asynchronously to ensure that the task has non empty description (assuming task
// position information was set), as KeyboardQuickSwitch view may request accessibility
// focus to be moved to the task when the quick switch UI gets shown. The description will
// be updated once the task metadata has been loaded - the delay should be very short, and
// the content description when task titles are not available still gives some useful
// information to the user (the task's position in the list).
updateContentDesctiptionForTasks(task1, task2);
if (iconUpdateFunction == null) {
applyIcon(mIcon1, task1);
applyIcon(mIcon2, task2);
setContentDescription(task2 == null
? task1.titleDescription
: getContext().getString(
R.string.quick_switch_split_task,
task1.titleDescription,
task2.titleDescription));
return;
}
iconUpdateFunction.updateIconInBackground(task1, t -> {
applyIcon(mIcon1, task1);
if (task2 != null) {
return;
}
updateContentDesctiptionForTasks(task1, null);
setContentDescription(task1.titleDescription);
});
if (task2 == null) {
return;
}
iconUpdateFunction.updateIconInBackground(task2, t -> {
applyIcon(mIcon2, task2);
updateContentDesctiptionForTasks(task1, task2);
setContentDescription(getContext().getString(
R.string.quick_switch_split_task,
task1.titleDescription,
task2.titleDescription));
});
}
/**
* Initializes information about the task's position within the parent container context - used
* to add position information to the view's content description.
* Should be called before associating the view with tasks.
*
* @param index The view's 0-based index within the parent task container.
* @param totalTasks The total number of tasks in the parent task container.
*/
protected void setPositionInformation(int index, int totalTasks) {
mIndexInParent = index;
mTotalTasksInParent = totalTasks;
}
protected void setThumbnailsForSplitTasks(
@NonNull Task task1,
@Nullable Task task2,
@Nullable ThumbnailUpdateFunction thumbnailUpdateFunction,
@Nullable IconUpdateFunction iconUpdateFunction,
@Nullable SplitBounds splitBounds) {
setThumbnails(task1, task2, thumbnailUpdateFunction, iconUpdateFunction);
if (splitBounds == null) {
return;
}
final boolean isLeftRightSplit = !splitBounds.appsStackedVertically;
final float leftOrTopTaskPercent = splitBounds.getLeftTopTaskPercent();
ConstraintLayout.LayoutParams leftTopParams = (ConstraintLayout.LayoutParams)
mThumbnailView1.getLayoutParams();
ConstraintLayout.LayoutParams rightBottomParams = (ConstraintLayout.LayoutParams)
mThumbnailView2.getLayoutParams();
if (isLeftRightSplit) {
// Set thumbnail view ratio in left right split mode.
leftTopParams.width = 0; // Set width to 0dp, so it uses the constraint dimension ratio.
leftTopParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT;
leftTopParams.matchConstraintPercentWidth = leftOrTopTaskPercent;
leftTopParams.leftToLeft = ConstraintLayout.LayoutParams.PARENT_ID;
leftTopParams.rightToLeft = R.id.thumbnail_2;
mThumbnailView1.setLayoutParams(leftTopParams);
rightBottomParams.width = 0;
rightBottomParams.height = ConstraintLayout.LayoutParams.MATCH_PARENT;
rightBottomParams.matchConstraintPercentWidth = 1 - leftOrTopTaskPercent;
rightBottomParams.leftToRight = R.id.thumbnail_1;
rightBottomParams.rightToRight = ConstraintLayout.LayoutParams.PARENT_ID;
mThumbnailView2.setLayoutParams(rightBottomParams);
} else {
// Set thumbnail view ratio in top bottom split mode.
leftTopParams.height = 0;
leftTopParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT;
leftTopParams.matchConstraintPercentHeight = leftOrTopTaskPercent;
leftTopParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
leftTopParams.bottomToTop = R.id.thumbnail_2;
mThumbnailView1.setLayoutParams(leftTopParams);
rightBottomParams.height = 0;
rightBottomParams.width = ConstraintLayout.LayoutParams.MATCH_PARENT;
rightBottomParams.matchConstraintPercentHeight = 1 - leftOrTopTaskPercent;
rightBottomParams.topToBottom = R.id.thumbnail_1;
rightBottomParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
mThumbnailView2.setLayoutParams(rightBottomParams);
}
}
private void applyThumbnail(
@Nullable ImageView thumbnailView,
@Nullable Task task,
@@ -272,8 +185,8 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout {
applyThumbnail(thumbnailView, task.colorBackground, task.thumbnail);
return;
}
updateFunction.updateThumbnailInBackground(task, thumbnailData ->
applyThumbnail(thumbnailView, task.colorBackground, thumbnailData));
updateFunction.updateThumbnailInBackground(task,
thumbnailData -> applyThumbnail(thumbnailView, task.colorBackground, thumbnailData));
}
private void applyThumbnail(
@@ -305,28 +218,6 @@ public class KeyboardQuickSwitchTaskView extends ConstraintLayout {
constantState.newDrawable(getResources(), getContext().getTheme()));
}
/**
* Updates the task view's content description to reflect tasks represented by the view.
*/
private void updateContentDesctiptionForTasks(@NonNull Task task1, @Nullable Task task2) {
String tasksDescription = task1.titleDescription == null || task2 == null
? task1.titleDescription
: getContext().getString(
R.string.quick_switch_split_task,
task1.titleDescription,
task2.titleDescription);
if (mIndexInParent < 0) {
setContentDescription(tasksDescription);
return;
}
setContentDescription(
getContext().getString(R.string.quick_switch_task_with_position_in_parent,
tasksDescription != null ? tasksDescription : "",
mIndexInParent + 1,
mTotalTasksInParent));
}
protected interface ThumbnailUpdateFunction {
void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback);
@@ -17,6 +17,8 @@ package com.android.launcher3.taskbar;
import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID;
import static com.android.launcher3.taskbar.KeyboardQuickSwitchController.MAX_TASKS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -30,57 +32,44 @@ import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.animation.Interpolator;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.window.OnBackInvokedDispatcher;
import android.window.WindowOnBackInvokedDispatcher;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.res.ResourcesCompat;
import com.android.app.animation.Interpolators;
import com.android.internal.jank.Cuj;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.SingleTask;
import com.android.quickstep.util.SplitTask;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.wm.shell.shared.TypefaceUtils;
import com.android.wm.shell.shared.TypefaceUtils.FontFamily;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
/**
* View that allows quick switching between recent tasks.
*
* Can be access via:
* - keyboard alt-tab
* - alt-shift-tab
* - taskbar overflow button
* View that allows quick switching between recent tasks through keyboard
* alt-tab and alt-shift-tab
* commands.
*/
public class KeyboardQuickSwitchView extends ConstraintLayout {
private static final long OUTLINE_ANIMATION_DURATION_MS = 333;
private static final float OUTLINE_START_HEIGHT_FACTOR = 0.45f;
private static final float OUTLINE_START_RADIUS_FACTOR = 0.25f;
private static final Interpolator OPEN_OUTLINE_INTERPOLATOR =
Interpolators.EMPHASIZED_DECELERATE;
private static final Interpolator CLOSE_OUTLINE_INTERPOLATOR =
Interpolators.EMPHASIZED_ACCELERATE;
private static final Interpolator OPEN_OUTLINE_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE;
private static final Interpolator CLOSE_OUTLINE_INTERPOLATOR = Interpolators.EMPHASIZED_ACCELERATE;
private static final long ALPHA_ANIMATION_DURATION_MS = 83;
private static final long ALPHA_ANIMATION_START_DELAY_MS = 67;
@@ -90,10 +79,8 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
private static final float CONTENT_START_TRANSLATION_X_DP = 32;
private static final float CONTENT_START_TRANSLATION_Y_DP = 40;
private static final Interpolator OPEN_TRANSLATION_X_INTERPOLATOR = Interpolators.EMPHASIZED;
private static final Interpolator OPEN_TRANSLATION_Y_INTERPOLATOR =
Interpolators.EMPHASIZED_DECELERATE;
private static final Interpolator CLOSE_TRANSLATION_Y_INTERPOLATOR =
Interpolators.EMPHASIZED_ACCELERATE;
private static final Interpolator OPEN_TRANSLATION_Y_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE;
private static final Interpolator CLOSE_TRANSLATION_Y_INTERPOLATOR = Interpolators.EMPHASIZED_ACCELERATE;
private static final long CONTENT_ALPHA_ANIMATION_DURATION_MS = 83;
private static final long CONTENT_ALPHA_ANIMATION_START_DELAY_MS = 83;
@@ -106,25 +93,15 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
private HorizontalScrollView mScrollView;
private ConstraintLayout mContent;
private boolean mSupportsScrollArrows = false;
private ImageButton mStartScrollArrow;
private ImageButton mEndScrollArrow;
private int mTaskViewBorderWidth;
private int mTaskViewRadius;
private int mTaskViewWidth;
private int mTaskViewHeight;
private int mSpacing;
private int mSmallSpacing;
private int mOutlineRadius;
private boolean mIsRtl;
private int mOverviewTaskIndex = -1;
private int mDesktopTaskIndex = -1;
@Nullable
private AnimatorSet mOpenAnimation;
private boolean mIsBackCallbackRegistered = false;
@Nullable
private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks;
@@ -147,75 +124,26 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mViewCallbacks != null) {
mViewCallbacks.onViewDetchedFromWindow();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mNoRecentItemsPane = findViewById(R.id.no_recent_items_pane);
mScrollView = findViewById(R.id.scroll_view);
mContent = findViewById(R.id.content);
mStartScrollArrow = findViewById(R.id.scroll_button_start);
mEndScrollArrow = findViewById(R.id.scroll_button_end);
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
Resources resources = getResources();
mTaskViewWidth = resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_taskview_width);
mTaskViewHeight = resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_taskview_height);
mSpacing = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_spacing);
mSmallSpacing = resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_view_small_spacing);
mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius);
mTaskViewBorderWidth = resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_border_width);
mTaskViewRadius = resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_task_view_radius);
mIsRtl = Utilities.isRtl(resources);
TypefaceUtils.setTypeface(
mNoRecentItemsPane.findViewById(R.id.no_recent_items_text),
FontFamily.GSF_LABEL_LARGE);
}
private void registerOnBackInvokedCallback() {
OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
if (isOnBackInvokedCallbackEnabled(dispatcher)
&& !mIsBackCallbackRegistered) {
dispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_OVERLAY, mViewCallbacks.onBackInvokedCallback);
mIsBackCallbackRegistered = true;
}
}
private void unregisterOnBackInvokedCallback() {
OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
if (isOnBackInvokedCallbackEnabled(dispatcher)
&& mIsBackCallbackRegistered) {
dispatcher.unregisterOnBackInvokedCallback(
mViewCallbacks.onBackInvokedCallback);
mIsBackCallbackRegistered = false;
}
}
private boolean isOnBackInvokedCallbackEnabled(OnBackInvokedDispatcher dispatcher) {
return dispatcher instanceof WindowOnBackInvokedDispatcher
&& ((WindowOnBackInvokedDispatcher) dispatcher).isOnBackInvokedCallbackEnabled()
&& mViewCallbacks != null;
}
private KeyboardQuickSwitchTaskView createAndAddTaskView(
int index,
boolean isFinalView,
boolean useSmallStartSpacing,
@LayoutRes int resId,
@NonNull LayoutInflater layoutInflater,
@Nullable View previousView) {
@@ -224,7 +152,7 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
taskView.setId(View.generateViewId());
taskView.setOnClickListener(v -> mViewCallbacks.launchTaskAt(index));
LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
LayoutParams lp = new LayoutParams(mTaskViewWidth, mTaskViewHeight);
// Create a left-to-right ordering of views (or right-to-left in RTL locales)
if (previousView != null) {
lp.startToEnd = previousView.getId();
@@ -234,9 +162,10 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
lp.topToTop = PARENT_ID;
lp.bottomToBottom = PARENT_ID;
// Add spacing between views
lp.setMarginStart(useSmallStartSpacing ? mSmallSpacing : mSpacing);
lp.setMarginStart(mSpacing);
if (isFinalView) {
// Add spacing to the end of the final view so that scrolling ends with some padding.
// Add spacing to the end of the final view so that scrolling ends with some
// padding.
lp.endToEnd = PARENT_ID;
lp.setMarginEnd(mSpacing);
lp.horizontalBias = 1f;
@@ -253,93 +182,67 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
int numHiddenTasks,
boolean updateTasks,
int currentFocusIndexOverride,
@NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks,
boolean useDesktopTaskView) {
mContent.removeAllViews();
@NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks) {
mViewCallbacks = viewCallbacks;
Resources resources = context.getResources();
Resources.Theme theme = context.getTheme();
View previousTaskView = null;
LayoutInflater layoutInflater = LayoutInflater.from(context);
int tasksToDisplay = groupTasks.size();
int tasksToDisplay = Math.min(MAX_TASKS, groupTasks.size());
for (int i = 0; i < tasksToDisplay; i++) {
GroupTask groupTask = groupTasks.get(i);
KeyboardQuickSwitchTaskView currentTaskView = createAndAddTaskView(
i,
/* isFinalView= */ i == tasksToDisplay - 1
&& numHiddenTasks == 0 && !useDesktopTaskView,
/* useSmallStartSpacing= */ false,
mViewCallbacks.isAspectRatioSquare()
? R.layout.keyboard_quick_switch_taskview_square
/* isFinalView= */ i == tasksToDisplay - 1 && numHiddenTasks == 0,
groupTask instanceof DesktopTask
? R.layout.keyboard_quick_switch_textonly_taskview
: R.layout.keyboard_quick_switch_taskview,
layoutInflater,
previousTaskView);
Task task1;
Task task2;
if (groupTask instanceof SplitTask splitTask) {
task1 = splitTask.getTopLeftTask();
task2 = splitTask.getBottomRightTask();
} else if (groupTask instanceof SingleTask singleTask) {
task1 = singleTask.getTask();
task2 = null;
if (groupTask instanceof DesktopTask desktopTask) {
HashMap<String, Integer> args = new HashMap<>();
args.put("count", desktopTask.tasks.size());
currentTaskView.<ImageView>findViewById(R.id.icon).setImageDrawable(
ResourcesCompat.getDrawable(resources, R.drawable.ic_desktop, theme));
currentTaskView.<TextView>findViewById(R.id.text).setText(new MessageFormat(
resources.getString(R.string.quick_switch_desktop),
Locale.getDefault()).format(args));
} else {
continue;
currentTaskView.setThumbnails(
groupTask.task1,
groupTask.task2,
updateTasks ? mViewCallbacks::updateThumbnailInBackground : null,
updateTasks ? mViewCallbacks::updateIconInBackground : null);
}
currentTaskView.setPositionInformation(i, tasksToDisplay);
currentTaskView.setThumbnailsForSplitTasks(
task1,
task2,
updateTasks ? mViewCallbacks::updateThumbnailInBackground : null,
updateTasks ? mViewCallbacks::updateIconInBackground : null,
groupTask instanceof SplitTask splitTask ? splitTask.getSplitBounds() : null);
previousTaskView = currentTaskView;
}
if (numHiddenTasks > 0) {
HashMap<String, Integer> args = new HashMap<>();
args.put("count", numHiddenTasks);
mOverviewTaskIndex = getTaskCount();
View overviewButton = createAndAddTaskView(
mOverviewTaskIndex,
/* isFinalView= */ !useDesktopTaskView,
/* useSmallStartSpacing= */ false,
R.layout.keyboard_quick_switch_overview_taskview,
MAX_TASKS,
/* isFinalView= */ true,
R.layout.keyboard_quick_switch_textonly_taskview,
layoutInflater,
previousTaskView);
overviewButton.<TextView>findViewById(R.id.large_text).setText(
String.format(Locale.getDefault(), "%d", numHiddenTasks));
overviewButton.<TextView>findViewById(R.id.small_text).setText(new MessageFormat(
overviewButton.<ImageView>findViewById(R.id.icon).setImageDrawable(
ResourcesCompat.getDrawable(resources, R.drawable.view_carousel, theme));
overviewButton.<TextView>findViewById(R.id.text).setText(new MessageFormat(
resources.getString(R.string.quick_switch_overflow),
Locale.getDefault()).format(args));
previousTaskView = overviewButton;
}
if (useDesktopTaskView) {
mDesktopTaskIndex = getTaskCount();
View desktopButton = createAndAddTaskView(
mDesktopTaskIndex,
/* isFinalView= */ true,
/* useSmallStartSpacing= */ numHiddenTasks > 0,
R.layout.keyboard_quick_switch_desktop_taskview,
layoutInflater,
previousTaskView);
desktopButton.<TextView>findViewById(R.id.small_text).setText(
resources.getString(R.string.quick_switch_desktop));
}
mDisplayingRecentTasks = !groupTasks.isEmpty() || useDesktopTaskView;
mDisplayingRecentTasks = !groupTasks.isEmpty();
getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
registerOnBackInvokedCallback();
animateOpen(currentFocusIndexOverride);
getViewTreeObserver().removeOnGlobalLayoutListener(this);
@@ -347,120 +250,6 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
});
}
void enableScrollArrowSupport() {
if (mSupportsScrollArrows) {
return;
}
mSupportsScrollArrows = true;
if (mIsRtl) {
mStartScrollArrow.setContentDescription(
getResources().getString(R.string.quick_switch_scroll_arrow_right));
mEndScrollArrow.setContentDescription(
getResources().getString(R.string.quick_switch_scroll_arrow_left));
}
mStartScrollArrow.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mIsRtl) {
runScrollCommand(false, () -> {
mScrollView.smoothScrollBy(mScrollView.getWidth(), 0);
});
} else {
runScrollCommand(false, () -> {
mScrollView.smoothScrollBy(-mScrollView.getWidth(), 0);
});
}
}
});
mEndScrollArrow.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mIsRtl) {
runScrollCommand(false, () -> {
mScrollView.smoothScrollBy(-mScrollView.getWidth(), 0);
});
} else {
runScrollCommand(false, () -> {
mScrollView.smoothScrollBy(mScrollView.getWidth(), 0);
});
}
}
});
// Add listeners to disable arrow buttons when the scroll view cannot be further scrolled in
// the associated direction.
mScrollView.setOnScrollChangeListener(new OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX,
int oldScrollY) {
updateArrowButtonsEnabledState();
}
});
// Update scroll view outline to clip its contents with rounded corners.
mScrollView.setClipToOutline(true);
mScrollView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
int spacingWithoutBorder = mSpacing - mTaskViewBorderWidth;
outline.setRoundRect(spacingWithoutBorder,
spacingWithoutBorder, view.getWidth() - spacingWithoutBorder,
view.getHeight() - spacingWithoutBorder,
mTaskViewRadius);
}
});
}
private void updateArrowButtonsEnabledState() {
if (!mDisplayingRecentTasks) {
return;
}
int scrollX = mScrollView.getScrollX();
if (mIsRtl) {
mEndScrollArrow.setEnabled(scrollX > 0);
mStartScrollArrow.setEnabled(scrollX < mContent.getWidth() - mScrollView.getWidth());
} else {
mStartScrollArrow.setEnabled(scrollX > 0);
mEndScrollArrow.setEnabled(scrollX < mContent.getWidth() - mScrollView.getWidth());
}
}
int getOverviewTaskIndex() {
return mOverviewTaskIndex;
}
int getDesktopTaskIndex() {
return mDesktopTaskIndex;
}
void resetViewCallbacks() {
// Unregister the back invoked callback after the view is closed and before the
// mViewCallbacks is reset.
unregisterOnBackInvokedCallback();
mViewCallbacks = null;
}
private void animateDisplayedContentForClose(View view, AnimatorSet animator) {
Animator translationYAnimation = ObjectAnimator.ofFloat(
view,
TRANSLATION_Y,
0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP));
translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR);
animator.play(translationYAnimation);
Animator contentAlphaAnimation = ObjectAnimator.ofFloat(view, ALPHA, 1f, 0f);
contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
animator.play(contentAlphaAnimation);
}
protected Animator getCloseAnimation() {
AnimatorSet closeAnimation = new AnimatorSet();
@@ -475,11 +264,17 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
closeAnimation.play(alphaAnimation);
View displayedContent = mDisplayingRecentTasks ? mScrollView : mNoRecentItemsPane;
animateDisplayedContentForClose(displayedContent, closeAnimation);
if (mSupportsScrollArrows) {
animateDisplayedContentForClose(mStartScrollArrow, closeAnimation);
animateDisplayedContentForClose(mEndScrollArrow, closeAnimation);
}
Animator translationYAnimation = ObjectAnimator.ofFloat(
displayedContent,
TRANSLATION_Y,
0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP));
translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR);
closeAnimation.play(translationYAnimation);
Animator contentAlphaAnimation = ObjectAnimator.ofFloat(displayedContent, ALPHA, 1f, 0f);
contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
closeAnimation.play(contentAlphaAnimation);
closeAnimation.addListener(new AnimatorListenerAdapter() {
@Override
@@ -494,42 +289,12 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
return closeAnimation;
}
private void animateDisplayedContentForOpen(View view, AnimatorSet animator) {
Animator translationXAnimation = ObjectAnimator.ofFloat(
view,
TRANSLATION_X,
-Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0);
translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS);
translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR);
animator.play(translationXAnimation);
Animator translationYAnimation = ObjectAnimator.ofFloat(
view,
TRANSLATION_Y,
-Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0);
translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR);
animator.play(translationYAnimation);
view.setAlpha(0.0f);
Animator contentAlphaAnimation = ObjectAnimator.ofFloat(view, ALPHA, 0f,
1f);
contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS);
contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
animator.play(contentAlphaAnimation);
}
protected void animateOpen(int currentFocusIndexOverride) {
private void animateOpen(int currentFocusIndexOverride) {
if (mOpenAnimation != null) {
// Restart animation since currentFocusIndexOverride can change the initial scroll.
// Restart animation since currentFocusIndexOverride can change the initial
// scroll.
mOpenAnimation.cancel();
}
// Reset the alpha for the case where the KQS view is opened before.
setAlpha(0);
mScrollView.setAlpha(0);
mNoRecentItemsPane.setAlpha(0);
mOpenAnimation = new AnimatorSet();
Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(1f);
@@ -541,23 +306,32 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
mOpenAnimation.play(alphaAnimation);
View displayedContent = mDisplayingRecentTasks ? mScrollView : mNoRecentItemsPane;
animateDisplayedContentForOpen(displayedContent, mOpenAnimation);
if (mSupportsScrollArrows) {
animateDisplayedContentForOpen(mStartScrollArrow, mOpenAnimation);
animateDisplayedContentForOpen(mEndScrollArrow, mOpenAnimation);
}
Animator translationXAnimation = ObjectAnimator.ofFloat(
displayedContent,
TRANSLATION_X,
-Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0);
translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS);
translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR);
mOpenAnimation.play(translationXAnimation);
Animator translationYAnimation = ObjectAnimator.ofFloat(
displayedContent,
TRANSLATION_Y,
-Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0);
translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR);
mOpenAnimation.play(translationYAnimation);
Animator contentAlphaAnimation = ObjectAnimator.ofFloat(displayedContent, ALPHA, 0f, 1f);
contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS);
contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
mOpenAnimation.play(contentAlphaAnimation);
ViewOutlineProvider outlineProvider = getOutlineProvider();
int defaultFocusedTaskIndex = Math.min(
getTaskCount() - 1,
currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride);
mOpenAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
InteractionJankMonitorWrapper.begin(
KeyboardQuickSwitchView.this, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
setClipToPadding(false);
setOutlineProvider(new ViewOutlineProvider() {
@Override
@@ -584,39 +358,14 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
OPEN_OUTLINE_INTERPOLATOR));
}
});
if (mSupportsScrollArrows) {
mScrollView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (mScrollView.getWidth() == 0) {
return;
}
if (mContent.getWidth() > mScrollView.getWidth()) {
mStartScrollArrow.setVisibility(VISIBLE);
mEndScrollArrow.setVisibility(VISIBLE);
updateArrowButtonsEnabledState();
}
mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(
this);
}
});
}
animateFocusMove(-1, defaultFocusedTaskIndex);
animateFocusMove(-1, Math.min(
mContent.getChildCount() - 1,
currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride));
displayedContent.setVisibility(VISIBLE);
setVisibility(VISIBLE);
requestFocus();
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
@@ -624,12 +373,6 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
setOutlineProvider(outlineProvider);
invalidateOutline();
mOpenAnimation = null;
InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
View focusedTask = getTaskAt(defaultFocusedTaskIndex);
if (focusedTask != null) {
focusedTask.requestAccessibilityFocus();
}
}
});
@@ -661,7 +404,8 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
int firstVisibleTaskIndex = toIndex == 0
? toIndex
: getTaskAt(toIndex - 1) == null
? toIndex : toIndex - 1;
? toIndex
: toIndex - 1;
// Scroll so that the previous task view is truncated as a visual hint that
// there are more tasks
initializeScroll(
@@ -792,9 +536,8 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
}
private boolean shouldScroll(@NonNull View targetTask, boolean shouldTruncateTarget) {
boolean isTargetTruncated =
targetTask.getRight() + mSpacing > mScrollView.getScrollX() + mScrollView.getWidth()
|| Math.max(0, targetTask.getLeft() - mSpacing) < mScrollView.getScrollX();
boolean isTargetTruncated = targetTask.getRight() + mSpacing > mScrollView.getScrollX() + mScrollView.getWidth()
|| Math.max(0, targetTask.getLeft() - mSpacing) < mScrollView.getScrollX();
return isTargetTruncated && !shouldTruncateTarget;
}
@@ -816,11 +559,8 @@ public class KeyboardQuickSwitchView extends ConstraintLayout {
@Nullable
protected KeyboardQuickSwitchTaskView getTaskAt(int index) {
return !mDisplayingRecentTasks || index < 0 || index >= getTaskCount()
? null : (KeyboardQuickSwitchTaskView) mContent.getChildAt(index);
}
public int getTaskCount() {
return mContent.getChildCount();
return !mDisplayingRecentTasks || index < 0 || index >= mContent.getChildCount()
? null
: (KeyboardQuickSwitchTaskView) mContent.getChildAt(index);
}
}
@@ -15,42 +15,30 @@
*/
package com.android.launcher3.taskbar;
import static com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType.UNMINIMIZE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.res.Resources;
import android.view.Gravity;
import android.app.ActivityOptions;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.animation.AnimationUtils;
import android.window.OnBackInvokedCallback;
import android.window.RemoteTransition;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.jank.Cuj;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.desktop.DesktopAppLaunchTransition;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.SingleTask;
import com.android.quickstep.util.SlideInRemoteTransition;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.wm.shell.shared.desktopmode.DesktopTaskToFrontReason;
import java.io.PrintWriter;
import java.util.List;
@@ -73,10 +61,6 @@ public class KeyboardQuickSwitchViewController {
private int mCurrentFocusIndex = -1;
private boolean mOnDesktop;
private boolean mWasDesktopTaskFilteredOut;
private boolean mWasOpenedFromTaskbar;
private boolean mDetachingFromWindow = false;
protected KeyboardQuickSwitchViewController(
@NonNull TaskbarControllers controllers,
@@ -93,36 +77,14 @@ public class KeyboardQuickSwitchViewController {
return mCurrentFocusIndex;
}
protected boolean wasOpenedFromTaskbar() {
return mWasOpenedFromTaskbar;
}
protected void openQuickSwitchView(
@NonNull List<GroupTask> tasks,
int numHiddenTasks,
boolean updateTasks,
int currentFocusIndexOverride,
boolean onDesktop,
boolean hasDesktopTask,
boolean wasDesktopTaskFilteredOut,
boolean wasOpenedFromTaskbar) {
final boolean isTransientTaskBar = mControllers.taskbarActivityContext.isTransientTaskbar();
positionView(wasOpenedFromTaskbar, isTransientTaskBar);
// Keep the taskbar unstashed if the KQS is opened.
if (wasOpenedFromTaskbar && isTransientTaskBar) {
mControllers.taskbarStashController.updateTaskbarTimeout(/* isAutohideSuspended= */
true);
}
boolean onDesktop) {
mOverlayContext.getDragLayer().addView(mKeyboardQuickSwitchView);
mOnDesktop = onDesktop;
mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut;
mWasOpenedFromTaskbar = wasOpenedFromTaskbar;
if (Flags.taskbarOverflow() && wasOpenedFromTaskbar) {
mKeyboardQuickSwitchView.enableScrollArrowSupport();
}
mKeyboardQuickSwitchView.applyLoadPlan(
mOverlayContext,
@@ -130,66 +92,7 @@ public class KeyboardQuickSwitchViewController {
numHiddenTasks,
updateTasks,
currentFocusIndexOverride,
mViewCallbacks,
/* useDesktopTaskView= */ !onDesktop && hasDesktopTask);
}
protected void updateQuickSwitchView(
@NonNull List<GroupTask> tasks,
int numHiddenTasks,
int currentFocusIndexOverride,
boolean hasDesktopTask,
boolean wasDesktopTaskFilteredOut) {
mWasDesktopTaskFilteredOut = wasDesktopTaskFilteredOut;
mKeyboardQuickSwitchView.applyLoadPlan(
mOverlayContext,
tasks,
numHiddenTasks,
/* updateTasks= */ true,
currentFocusIndexOverride,
mViewCallbacks,
/* useDesktopTaskView= */ !mOnDesktop && hasDesktopTask);
}
protected void positionView(boolean wasOpenedFromTaskbar, boolean isTransientTaskbar) {
if (!wasOpenedFromTaskbar) {
// Keep the default positioning.
return;
}
BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(
mKeyboardQuickSwitchView.getLayoutParams());
final Resources resources = mKeyboardQuickSwitchView.getResources();
final int marginHorizontal = resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_margin_ends);
final DeviceProfile dp = mControllers.taskbarActivityContext.getDeviceProfile();
// Calculate the additional margin space that the KQS should move up for the transient
// taskbar. The value of spaceForTaskbar is the distance between the bottom of the KQS
// view with 0 bottom margin to the top of the transient taskbar view.
final int spaceForTaskbar = isTransientTaskbar ? dp.taskbarHeight + dp.taskbarBottomMargin
- dp.stashedTaskbarHeight : 0;
final int marginBottom = spaceForTaskbar + resources.getDimensionPixelSize(
R.dimen.keyboard_quick_switch_margin_bottom);
lp.setMargins(marginHorizontal, 0, marginHorizontal, marginBottom);
lp.width = BaseDragLayer.LayoutParams.WRAP_CONTENT;
lp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
mKeyboardQuickSwitchView.setLayoutParams(lp);
}
protected void updateLayoutForSurface(boolean updateLayoutFromTaskbar,
int currentFocusIndexOverride) {
BaseDragLayer.LayoutParams lp =
(BaseDragLayer.LayoutParams) mKeyboardQuickSwitchView.getLayoutParams();
if (updateLayoutFromTaskbar) {
lp.width = BaseDragLayer.LayoutParams.WRAP_CONTENT;
} else {
lp.width = BaseDragLayer.LayoutParams.MATCH_PARENT;
}
mKeyboardQuickSwitchView.animateOpen(currentFocusIndexOverride);
mViewCallbacks);
}
boolean isCloseAnimationRunning() {
@@ -198,29 +101,18 @@ public class KeyboardQuickSwitchViewController {
protected void closeQuickSwitchView(boolean animate) {
if (isCloseAnimationRunning()) {
// Let currently-running animation finish.
if (!animate) {
mCloseAnimation.end();
}
// Let currently-running animation finish.
return;
}
mControllerCallbacks.onCloseStarted();
if (!animate) {
InteractionJankMonitorWrapper.begin(
mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
onCloseComplete();
return;
}
mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation();
mCloseAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
InteractionJankMonitorWrapper.begin(
mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
}
});
mCloseAnimation.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete));
mCloseAnimation.start();
}
@@ -239,7 +131,7 @@ public class KeyboardQuickSwitchViewController {
}
// If the user quick switches too quickly, updateCurrentFocusIndex might not have run.
return launchTaskAt(mControllerCallbacks.isFirstTaskRunning()
&& mKeyboardQuickSwitchView.getTaskCount() > 1 ? 1 : 0);
&& mControllerCallbacks.getTaskCount() > 1 ? 1 : 0);
}
private int launchTaskAt(int index) {
@@ -247,33 +139,6 @@ public class KeyboardQuickSwitchViewController {
// Ignore taps on task views and alt key unpresses while the close animation is running.
return -1;
}
if (index == mKeyboardQuickSwitchView.getOverviewTaskIndex()) {
// If there is a desktop task view, then we should account for it when focusing the
// first hidden non-desktop task view in recents view
return mOnDesktop ? 1 : (mWasDesktopTaskFilteredOut ? index + 1 : index);
}
Runnable onStartCallback = () -> InteractionJankMonitorWrapper.begin(
mKeyboardQuickSwitchView, Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
Runnable onFinishCallback = () -> InteractionJankMonitorWrapper.end(
Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
TaskbarActivityContext context = mControllers.taskbarActivityContext;
final RemoteTransition slideInTransition = new RemoteTransition(new SlideInRemoteTransition(
Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
context.getDeviceProfile().overviewPageSpacing,
QuickStepContract.getWindowCornerRadius(context),
AnimationUtils.loadInterpolator(
context, android.R.interpolator.fast_out_extra_slow_in),
onStartCallback,
onFinishCallback),
"SlideInTransition");
if (index == mKeyboardQuickSwitchView.getDesktopTaskIndex()) {
UI_HELPER_EXECUTOR.execute(() ->
SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
.showDesktopApps(
mKeyboardQuickSwitchView.getDisplay().getDisplayId(),
slideInTransition));
return -1;
}
// Even with a valid index, this can be null if the user tries to quick switch before the
// views have been added in the KeyboardQuickSwitchView.
GroupTask task = mControllerCallbacks.getTaskAt(index);
@@ -284,41 +149,44 @@ public class KeyboardQuickSwitchViewController {
// Ignore attempts to run the selected task if it is already running.
return -1;
}
RemoteTransition remoteTransition = slideInTransition;
boolean canUnminimizeDesktopTask = task instanceof SingleTask singleTask
&& mControllers.taskbarActivityContext.canUnminimizeDesktopTask(
singleTask.getTask().key.id);
if (mOnDesktop && canUnminimizeDesktopTask) {
// This app is being unminimized - use our own transition runner.
remoteTransition = new RemoteTransition(
new DesktopAppLaunchTransition(
context,
UNMINIMIZE,
Cuj.CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH,
MAIN_EXECUTOR
),
"DesktopKeyboardQuickSwitchUnminimize");
TaskbarActivityContext context = mControllers.taskbarActivityContext;
RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition(
Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
context.getDeviceProfile().overviewPageSpacing,
QuickStepContract.getWindowCornerRadius(context),
AnimationUtils.loadInterpolator(
context, android.R.interpolator.fast_out_extra_slow_in)),
"SlideInTransition");
if (task instanceof DesktopTask) {
UI_HELPER_EXECUTOR.execute(() ->
SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
.showDesktopApps(
mKeyboardQuickSwitchView.getDisplay().getDisplayId(),
remoteTransition));
} else if (mOnDesktop) {
UI_HELPER_EXECUTOR.execute(() ->
SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
.showDesktopApp(task.task1.key.id));
} else if (task.task2 == null) {
UI_HELPER_EXECUTOR.execute(() -> {
ActivityOptions activityOptions = mControllers.taskbarActivityContext
.makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
activityOptions.setRemoteTransition(remoteTransition);
ActivityManagerWrapper.getInstance().startActivityFromRecents(
task.task1.key, activityOptions);
});
} else {
mControllers.uiController.launchSplitTasks(task, remoteTransition);
}
mControllers.taskbarActivityContext.handleGroupTaskLaunch(
task,
remoteTransition,
mOnDesktop,
DesktopTaskToFrontReason.ALT_TAB,
onStartCallback,
onFinishCallback);
return -1;
}
private void onCloseComplete() {
mCloseAnimation = null;
// Reset the view callbacks to prevent `onDetachedFromWindow` getting called in response to
// the `removeView(mKeyboardQuickSwitchView)` call.
mKeyboardQuickSwitchView.resetViewCallbacks();
if (!mDetachingFromWindow) {
mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
}
mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
mControllerCallbacks.onCloseComplete();
InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_CLOSE);
}
protected void onDestroy() {
@@ -331,20 +199,9 @@ public class KeyboardQuickSwitchViewController {
pw.println(prefix + "\thasFocus=" + mKeyboardQuickSwitchView.hasFocus());
pw.println(prefix + "\tisCloseAnimationRunning=" + isCloseAnimationRunning());
pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex);
pw.println(prefix + "\tmOnDesktop=" + mOnDesktop);
pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut);
pw.println(prefix + "\tmWasOpenedFromTaskbar=" + mWasOpenedFromTaskbar);
}
/**
* @return True if the MotionEvent is over the {@link KeyboardQuickSwitchView}.
*/
protected boolean isEventOverKeyboardQuickSwitch(TaskbarOverlayDragLayer dl, MotionEvent ev) {
return dl.isEventOverView(mKeyboardQuickSwitchView, ev);
}
class ViewCallbacks {
public final OnBackInvokedCallback onBackInvokedCallback = () -> closeQuickSwitchView(true);
boolean onKeyUp(int keyCode, KeyEvent event, boolean isRTL, boolean allowTraversal) {
if (keyCode != KeyEvent.KEYCODE_TAB
@@ -369,7 +226,7 @@ public class KeyboardQuickSwitchViewController {
boolean traverseBackwards = (keyCode == KeyEvent.KEYCODE_TAB && event.isShiftPressed())
|| (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && isRTL)
|| (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && !isRTL);
int taskCount = mKeyboardQuickSwitchView.getTaskCount();
int taskCount = mControllerCallbacks.getTaskCount();
int toIndex = mCurrentFocusIndex == -1
// Focus the second-most recent app if possible
? (taskCount > 1 ? 1 : 0)
@@ -404,15 +261,5 @@ public class KeyboardQuickSwitchViewController {
void updateIconInBackground(Task task, Consumer<Task> callback) {
mControllerCallbacks.updateIconInBackground(task, callback);
}
boolean isAspectRatioSquare() {
return mControllerCallbacks.isAspectRatioSquare();
}
void onViewDetchedFromWindow() {
mDetachingFromWindow = true;
closeQuickSwitchView(false);
mDetachingFromWindow = false;
}
}
}
@@ -15,16 +15,12 @@
*/
package com.android.launcher3.taskbar;
import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
import static com.android.launcher3.Flags.syncAppLaunchWithTaskbarStash;
import static com.android.launcher3.QuickstepTransitionManager.TASKBAR_TO_APP_DURATION;
import static com.android.launcher3.QuickstepTransitionManager.TRANSIENT_TASKBAR_TRANSITION_DURATION;
import static com.android.launcher3.QuickstepTransitionManager.getTaskbarToHomeDuration;
import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
import static com.android.launcher3.taskbar.TaskbarEduTooltipControllerKt.TOOLTIP_STEP_FEATURES;
import static com.android.launcher3.taskbar.TaskbarLauncherStateController.FLAG_VISIBLE;
import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IGNORE_IN_APP;
import static com.android.quickstep.TaskAnimationManager.ENABLE_SHELL_TRANSITIONS;
import static com.android.window.flags2.Flags.enableDesktopWindowingWallpaperActivity;
import android.animation.Animator;
import android.animation.AnimatorSet;
@@ -35,32 +31,27 @@ import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
import com.android.launcher3.Hotseat;
import com.android.launcher3.LauncherState;
import com.android.launcher3.QuickstepTransitionManager;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.logging.InstanceId;
import com.android.launcher3.logging.InstanceIdSequence;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.taskbar.bubbles.BubbleBarController;
import com.android.launcher3.taskbar.bubbles.BubbleControllers;
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.GestureState;
import com.android.quickstep.HomeVisibilityState;
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.RecentsAnimationCallbacks;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.fallback.window.RecentsDisplayModel;
import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.util.SplitTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.TISBindHelper;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.RecentsViewContainer;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import java.io.PrintWriter;
import java.util.Arrays;
@@ -76,41 +67,32 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
public static final int ALL_APPS_PAGE_PROGRESS_INDEX = 1;
public static final int WIDGETS_PAGE_PROGRESS_INDEX = 2;
public static final int SYSUI_SURFACE_PROGRESS_INDEX = 3;
public static final int LAUNCHER_PAUSE_PROGRESS_INDEX = 4;
public static final int DISPLAY_PROGRESS_COUNT = 5;
public static final int DISPLAY_PROGRESS_COUNT = 4;
private final AnimatedFloat mTaskbarInAppDisplayProgress = new AnimatedFloat(
this::onInAppDisplayProgressChanged);
private final MultiPropertyFactory<AnimatedFloat> mTaskbarInAppDisplayProgressMultiProp =
new MultiPropertyFactory<>(mTaskbarInAppDisplayProgress,
AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max);
private final AnimatedFloat mLauncherPauseProgress = new AnimatedFloat(
this::onLauncherPauseProgressUpdate);
private final MultiPropertyFactory<AnimatedFloat> mTaskbarInAppDisplayProgressMultiProp = new MultiPropertyFactory<>(
mTaskbarInAppDisplayProgress,
AnimatedFloat.VALUE, DISPLAY_PROGRESS_COUNT, Float::max);
private final QuickstepLauncher mLauncher;
private final HomeVisibilityState mHomeState;
private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener =
dp -> {
onStashedInAppChanged(dp);
postAdjustHotseatForBubbleBar();
if (mControllers != null && mControllers.taskbarViewController != null) {
mControllers.taskbarViewController.onRotationChanged(dp);
}
};
private final HomeVisibilityState.VisibilityChangeListener mVisibilityChangeListener =
this::onLauncherVisibilityChanged;
private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener = dp -> {
onStashedInAppChanged(dp);
if (mControllers != null && mControllers.taskbarViewController != null) {
mControllers.taskbarViewController.onRotationChanged(dp);
}
};
private final HomeVisibilityState.VisibilityChangeListener mVisibilityChangeListener = this::onLauncherVisibilityChanged;
// Initialized in init.
private final TaskbarLauncherStateController
mTaskbarLauncherStateController = new TaskbarLauncherStateController();
// When overview-in-a-window is enabled, that window is the container, else it is mLauncher.
private RecentsViewContainer mRecentsViewContainer;
private final TaskbarLauncherStateController mTaskbarLauncherStateController = new TaskbarLauncherStateController();
public LauncherTaskbarUIController(QuickstepLauncher launcher) {
mLauncher = launcher;
mHomeState = SystemUiProxy.INSTANCE.get(mLauncher).getHomeVisibilityState();
mHomeState = SystemUiProxy.INSTANCE.get(mLauncher).getHomeVisibilityState();
}
@Override
@@ -119,23 +101,13 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
mTaskbarLauncherStateController.init(mControllers, mLauncher,
mControllers.getSharedState().sysuiStateFlags);
final TaskbarActivityContext taskbarContext = mControllers.taskbarActivityContext;
if (RecentsWindowFlags.getEnableOverviewInWindow()) {
mRecentsViewContainer = RecentsDisplayModel.getINSTANCE()
.get(taskbarContext).getRecentsWindowManager(taskbarContext.getDisplayId());
}
if (mRecentsViewContainer == null) {
mRecentsViewContainer = mLauncher;
}
mLauncher.setTaskbarUIController(this);
if (mRecentsViewContainer != mLauncher) {
mRecentsViewContainer.setTaskbarUIController(this);
}
mLauncher.setTaskbarUIController(this);
mHomeState.addListener(mVisibilityChangeListener);
onLauncherVisibilityChanged(
Flags.useActivityOverlay()
? mHomeState.isHomeVisible() : mLauncher.hasBeenResumed(),
? mHomeState.isHomeVisible()
: mLauncher.hasBeenResumed(),
true /* fromInit */);
onStashedInAppChanged(mLauncher.getDeviceProfile());
@@ -143,8 +115,10 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
// Restore the in-app display progress from before Taskbar was recreated.
float[] prevProgresses = mControllers.getSharedState().inAppDisplayProgressMultiPropValues;
// Make a copy of the previous progress to set since updating the multiprop will update
// the property which also calls onInAppDisplayProgressChanged() which writes the current
// Make a copy of the previous progress to set since updating the multiprop will
// update
// the property which also calls onInAppDisplayProgressChanged() which writes
// the current
// values into the shared state
prevProgresses = Arrays.copyOf(prevProgresses, prevProgresses.length);
for (int i = 0; i < prevProgresses.length; i++) {
@@ -154,15 +128,12 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
@Override
protected void onDestroy() {
onLauncherVisibilityChanged(false /* isVisible */, true /* fromInitOrDestroy */);
mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
super.onDestroy();
onLauncherVisibilityChanged(false);
mTaskbarLauncherStateController.onDestroy();
mLauncher.setTaskbarUIController(null);
if (mRecentsViewContainer != mLauncher) {
mRecentsViewContainer.setTaskbarUIController(null);
}
mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
mHomeState.removeListener(mVisibilityChangeListener);
}
@@ -170,8 +141,9 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
if (mControllers != null) {
// Update our shared state so we can restore it if taskbar gets recreated.
for (int i = 0; i < DISPLAY_PROGRESS_COUNT; i++) {
mControllers.getSharedState().inAppDisplayProgressMultiPropValues[i] =
mTaskbarInAppDisplayProgressMultiProp.get(i).getValue();
mControllers
.getSharedState().inAppDisplayProgressMultiPropValues[i] = mTaskbarInAppDisplayProgressMultiProp
.get(i).getValue();
}
// Ensure nav buttons react to our latest state if necessary.
mControllers.navbarButtonsViewController.updateNavButtonTranslationY();
@@ -180,9 +152,8 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
@Override
protected boolean isTaskbarTouchable() {
// Touching down during animation to Hotseat will end the transition and allow the touch to
// go through to the Hotseat directly.
return !isAnimatingToHotseat();
return !(mTaskbarLauncherStateController.isAnimatingToLauncher()
&& mTaskbarLauncherStateController.isTaskbarAlignedWithHotseat());
}
public void setShouldDelayLauncherStateAnim(boolean shouldDelayLauncherStateAnim) {
@@ -190,25 +161,19 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
shouldDelayLauncherStateAnim);
}
@Override
public void stashHotseat(boolean stash) {
mTaskbarLauncherStateController.stashHotseat(stash);
}
@Override
public void unStashHotseatInstantly() {
mTaskbarLauncherStateController.unStashHotseatInstantly();
}
/**
* Adds the Launcher resume animator to the given animator set.
*
* This should be used to run a Launcher resume animation whose progress matches a
* This should be used to run a Launcher resume animation whose progress matches
* a
* swipe progress.
*
* @param placeholderDuration a placeholder duration to be used to ensure all full-length
* sub-animations are properly coordinated. This duration should not
* actually be used since this animation tracks a swipe progress.
* @param placeholderDuration a placeholder duration to be used to ensure all
* full-length
* sub-animations are properly coordinated. This
* duration should not
* actually be used since this animation tracks a
* swipe progress.
*/
protected void addLauncherVisibilityChangedAnimation(AnimatorSet animation,
int placeholderDuration) {
@@ -220,67 +185,51 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
}
/**
* Should be called from onResume() and onPause(), and animates the Taskbar accordingly.
* Should be called from onResume() and onPause(), and animates the Taskbar
* accordingly.
*/
@Override
public void onLauncherVisibilityChanged(boolean isVisible) {
if (DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(mLauncher)
&& mControllers.taskbarActivityContext.isPrimaryDisplay()) {
DisplayController.INSTANCE.get(mLauncher).notifyConfigChange();
}
onLauncherVisibilityChanged(isVisible, false /* fromInit */);
}
private void onLauncherVisibilityChanged(boolean isVisible, boolean fromInitOrDestroy) {
if (mControllers == null) {
return;
}
private void onLauncherVisibilityChanged(boolean isVisible, boolean fromInit) {
onLauncherVisibilityChanged(
isVisible,
fromInitOrDestroy,
fromInit,
/* startAnimation= */ true,
getTaskbarAnimationDuration(isVisible));
}
private int getTaskbarAnimationDuration(boolean isVisible) {
// fast animation duration since we will not be playing workspace reveal animation.
boolean shouldOverrideToFastAnimation = !isHotseatIconOnTopWhenAligned();
if (!Flags.predictiveBackToHomePolish()) {
shouldOverrideToFastAnimation |= mLauncher.getPredictiveBackToHomeInProgress();
}
boolean isPinned = mControllers.taskbarActivityContext.isPinnedTaskbar();
if (isVisible || isPinned) {
return getTaskbarToHomeDuration(shouldOverrideToFastAnimation, isPinned);
} else {
return (mControllers.taskbarActivityContext.isTransientTaskbar())
? TRANSIENT_TASKBAR_TRANSITION_DURATION : TASKBAR_TO_APP_DURATION;
}
DisplayController.isTransientTaskbar(mLauncher)
? TRANSIENT_TASKBAR_TRANSITION_DURATION
: (!isVisible
? QuickstepTransitionManager.TASKBAR_TO_APP_DURATION
: QuickstepTransitionManager.getTaskbarToHomeDuration()));
}
@Nullable
private Animator onLauncherVisibilityChanged(
boolean isVisible, boolean fromInitOrDestroy, boolean startAnimation, int duration) {
// Launcher is resumed during the swipe-to-overview gesture under shell-transitions, so
// avoid updating taskbar state in that situation (when it's non-interactive -- or
boolean isVisible, boolean fromInit, boolean startAnimation, int duration) {
// Launcher is resumed during the swipe-to-overview gesture under
// shell-transitions, so
// avoid updating taskbar state in that situation (when it's non-interactive --
// or
// "background") to avoid premature animations.
LauncherState state = mTaskbarLauncherStateController.getLauncherState();
boolean nonInteractiveState = state.hasFlag(FLAG_NON_INTERACTIVE)
&& !state.isTaskbarAlignedWithHotseat(mLauncher);
if (isVisible && (nonInteractiveState || mSkipLauncherVisibilityChange)) {
if (ENABLE_SHELL_TRANSITIONS && isVisible
&& mLauncher.getStateManager().getState().hasFlag(FLAG_NON_INTERACTIVE)
&& !mLauncher.getStateManager().getState().isTaskbarAlignedWithHotseat(mLauncher)) {
return null;
}
if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
&& mControllers.taskbarDesktopModeController
.isInDesktopModeAndNotInOverview(mLauncher.getDisplayId())) {
DesktopVisibilityController desktopController = LauncherActivityInterface.INSTANCE
.getDesktopVisibilityController();
if (!enableDesktopWindowingWallpaperActivity()
&& desktopController != null
&& desktopController.areDesktopTasksVisible()) {
// TODO: b/333533253 - Remove after flag rollout
isVisible = false;
}
mTaskbarLauncherStateController.updateStateForFlag(FLAG_VISIBLE, isVisible);
if (fromInitOrDestroy) {
if (fromInit) {
duration = 0;
}
return mTaskbarLauncherStateController.applyState(duration, startAnimation);
@@ -294,7 +243,8 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
@Override
public void refreshResumedState() {
onLauncherVisibilityChanged(Flags.useActivityOverlay()
? mHomeState.isHomeVisible() : mLauncher.hasBeenResumed());
? mHomeState.isHomeVisible()
: mLauncher.hasBeenResumed());
}
@Override
@@ -304,56 +254,19 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
}
}
private void postAdjustHotseatForBubbleBar() {
Hotseat hotseat = mLauncher.getHotseat();
if (hotseat == null || !isBubbleBarVisible()) return;
hotseat.post(() -> {
if (mControllers == null) return;
adjustHotseatForBubbleBar(isBubbleBarVisible());
});
}
private boolean isBubbleBarVisible() {
BubbleControllers bubbleControllers = mControllers.bubbleControllers.orElse(null);
return bubbleControllers != null
&& bubbleControllers.bubbleBarViewController.isBubbleBarVisible();
}
/**
* Create Taskbar animation when going from an app to Launcher as part of recents transition.
* {@inheritDoc}
* Create Taskbar animation when going from an app to Launcher as part of
* recents transition.
*
* @param toState If known, the state we will end up in when reaching
* Launcher.
* @param callbacks callbacks to track the recents animation lifecycle. The
* state change is
* automatically reset once the recents animation finishes
*/
@Override
public Animator getParallelAnimationToGestureEndTarget(
GestureState.GestureEndTarget gestureEndTarget, long duration,
RecentsAnimationCallbacks callbacks) {
return mTaskbarLauncherStateController.createAnimToLauncher(
LauncherActivityInterface.INSTANCE.stateFromGestureEndTarget(gestureEndTarget),
callbacks,
duration);
}
/**
* Create Taskbar animation to be played alongside the Launcher app launch animation.
*/
public @Nullable Animator createAnimToApp() {
if (!syncAppLaunchWithTaskbarStash()) {
return null;
}
TaskbarStashController stashController = mControllers.taskbarStashController;
stashController.updateStateForFlag(TaskbarStashController.FLAG_IN_APP, true);
return stashController.createApplyStateAnimator(stashController.getStashDuration());
}
/**
* Temporarily ignore FLAG_IN_APP for app launches to prevent premature taskbar stashing.
* This is needed because taskbar gets a signal to stash before we actually start the
* app launch animation.
*/
public void setIgnoreInAppFlagForSync(boolean enabled) {
if (syncAppLaunchWithTaskbarStash()) {
mControllers.taskbarStashController.updateStateForFlag(FLAG_IGNORE_IN_APP, enabled);
}
public Animator createAnimToLauncher(@NonNull LauncherState toState,
@NonNull RecentsAnimationCallbacks callbacks, long duration) {
return mTaskbarLauncherStateController.createAnimToLauncher(toState, callbacks, duration);
}
public void updateTaskbarLauncherStateGoingHome() {
@@ -362,12 +275,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
}
public boolean isDraggingItem() {
boolean bubblesDragging = false;
if (mControllers.bubbleControllers.isPresent()) {
bubblesDragging =
mControllers.bubbleControllers.get().bubbleDragController.isDragging();
}
return mControllers.taskbarDragController.isDragging() || bubblesDragging;
return mControllers.taskbarDragController.isDragging();
}
@Override
@@ -381,7 +289,8 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
}
/**
* Starts a Taskbar EDU flow, if the user should see one upon launching an application.
* Starts a Taskbar EDU flow, if the user should see one upon launching an
* application.
*/
public void showEduOnAppLaunch() {
if (!shouldShowEduOnAppLaunch()) {
@@ -391,7 +300,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
}
// Persistent features EDU tooltip.
if (!mControllers.taskbarActivityContext.isTransientTaskbar()) {
if (!DisplayController.isTransientTaskbar(mLauncher)) {
mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu();
return;
}
@@ -406,7 +315,8 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
}
/**
* Returns {@code true} if a Taskbar education should be shown on application launch.
* Returns {@code true} if a Taskbar education should be shown on application
* launch.
*/
public boolean shouldShowEduOnAppLaunch() {
if (Utilities.isRunningInTestHarness()) {
@@ -414,7 +324,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
}
// Persistent features EDU tooltip.
if (!mControllers.taskbarActivityContext.isTransientTaskbar()) {
if (!DisplayController.isTransientTaskbar(mLauncher)) {
return !OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.hasReachedMax(mLauncher);
}
@@ -431,7 +341,8 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
}
/**
* Animates Taskbar elements during a transition to a Launcher state that should use in-app
* Animates Taskbar elements during a transition to a Launcher state that should
* use in-app
* layouts.
*
* @param progress [0, 1]
@@ -444,22 +355,16 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
// This method can be called before init() is called.
return;
}
if (mControllers.uiController.isIconAlignedWithHotseat()) {
if (!mTaskbarLauncherStateController.isAnimatingToLauncher()) {
// Only animate the nav buttons while home and not animating home, otherwise let
// the TaskbarViewController handle it.
mControllers.navbarButtonsViewController
.getTaskbarNavButtonTranslationYForInAppDisplay()
.updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY()
* mTaskbarInAppDisplayProgress.value);
mControllers.navbarButtonsViewController
.getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
}
if (isBubbleBarEnabled()) {
mControllers.bubbleControllers.ifPresent(
c -> c.bubbleStashController.setInAppDisplayOverrideProgress(
mTaskbarInAppDisplayProgress.value));
}
if (mControllers.uiController.isIconAlignedWithHotseat()
&& !mTaskbarLauncherStateController.isAnimatingToLauncher()) {
// Only animate the nav buttons while home and not animating home, otherwise let
// the TaskbarViewController handle it.
mControllers.navbarButtonsViewController
.getTaskbarNavButtonTranslationYForInAppDisplay()
.updateValue(mLauncher.getDeviceProfile().getTaskbarOffsetY()
* mTaskbarInAppDisplayProgress.value);
mControllers.navbarButtonsViewController
.getOnTaskbarBackgroundNavButtonColorOverride().updateValue(progress);
}
}
@@ -504,18 +409,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
public boolean isHotseatIconOnTopWhenAligned() {
return mTaskbarLauncherStateController.isInHotseatOnTopStates()
&& mTaskbarInAppDisplayProgressMultiProp.get(MINUS_ONE_PAGE_PROGRESS_INDEX)
.getValue() == 0;
}
@Override
public boolean isAnimatingToHotseat() {
return mTaskbarLauncherStateController.isAnimatingToLauncher()
&& isIconAlignedWithHotseat();
}
@Override
public void endAnimationToHotseat() {
mTaskbarLauncherStateController.resetIconAlignment();
.getValue() == 0;
}
@Override
@@ -524,26 +418,21 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
}
@Override
protected void toggleAllApps(boolean focusSearch) {
boolean canToggleHomeAllApps = mLauncher.isResumed()
protected boolean canToggleHomeAllApps() {
return mLauncher.isResumed()
&& !mTaskbarLauncherStateController.isInOverviewUi()
&& !mLauncher.areDesktopTasksVisible();
if (canToggleHomeAllApps) {
mLauncher.toggleAllApps(focusSearch);
return;
}
super.toggleAllApps(focusSearch);
}
@Override
public RecentsView getRecentsView() {
return mRecentsViewContainer.getOverviewPanel();
return mLauncher.getOverviewPanel();
}
@Override
public void launchSplitTasks(
@NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) {
mLauncher.launchSplitTasks(splitTask, remoteTransition);
@NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) {
mLauncher.launchSplitTasks(groupTask, remoteTransition);
}
@Override
@@ -551,6 +440,12 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
mTaskbarLauncherStateController.resetIconAlignment();
}
@Nullable
@Override
protected TISBindHelper getTISBindHelper() {
return mLauncher.getTISBindHelper();
}
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
super.dumpLogs(prefix, pw);
@@ -564,9 +459,7 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
"MINUS_ONE_PAGE_PROGRESS_INDEX",
"ALL_APPS_PAGE_PROGRESS_INDEX",
"WIDGETS_PAGE_PROGRESS_INDEX",
"SYSUI_SURFACE_PROGRESS_INDEX",
"LAUNCHER_PAUSE_PROGRESS_INDEX");
pw.println(String.format("%s\tmRecentsWindowContainer=%s", prefix, mRecentsViewContainer));
"SYSUI_SURFACE_PROGRESS_INDEX");
mTaskbarLauncherStateController.dumpLogs(prefix + "\t", pw);
}
@@ -575,60 +468,4 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
protected String getTaskbarUIControllerName() {
return "LauncherTaskbarUIController";
}
@Override
public void onBubbleBarLocationAnimated(BubbleBarLocation location) {
mTaskbarLauncherStateController.onBubbleBarLocationChanged(location, /* animate = */ true);
mLauncher.setBubbleBarLocation(location);
}
@Override
public void onBubbleBarLocationUpdated(BubbleBarLocation location) {
mTaskbarLauncherStateController.onBubbleBarLocationChanged(location, /* animate = */ false);
mLauncher.setBubbleBarLocation(location);
}
@Override
public void onSwipeToUnstashTaskbar() {
// Once taskbar is unstashed, the user cannot return back to the overlay. We can
// clear it here to set the expected state once the user goes home.
if (mLauncher.getWorkspace().isOverlayShown()) {
mLauncher.getWorkspace().onOverlayScrollChanged(0);
}
}
/**
* Called when Launcher Activity resumed while staying at home.
* <p>
* Shift nav buttons up to at-home position.
*/
public void onLauncherResume() {
mLauncherPauseProgress.animateToValue(0.0f).start();
}
/**
* Called when Launcher Activity paused while staying at home.
* <p>
* To avoid UI clash between taskbar & bottom sheet, shift nav buttons down to in-app position.
*/
public void onLauncherPause() {
mLauncherPauseProgress.animateToValue(1.0f).start();
}
/**
* On launcher stop, avoid animating taskbar & overriding pre-existing animations.
*/
public void onLauncherStop() {
mLauncherPauseProgress.cancelAnimation();
mLauncherPauseProgress.updateValue(0.0f);
}
private void onLauncherPauseProgressUpdate() {
// If we are not aligned with hotseat, setting this will clobber the 3 button nav position.
// So in that case, treat the progress as 0 instead.
float pauseProgress = isIconAlignedWithHotseat() ? mLauncherPauseProgress.value : 0;
onTaskbarInAppDisplayProgressUpdate(pauseProgress, LAUNCHER_PAUSE_PROGRESS_INDEX);
}
}
File diff suppressed because it is too large Load Diff
@@ -47,7 +47,6 @@ public class StashedHandleView extends View {
private final int[] mTmpArr = new int[2];
private @Nullable ObjectAnimator mColorChangeAnim;
private Boolean mIsRegionDark;
public StashedHandleView(Context context) {
this(context, null);
@@ -96,11 +95,7 @@ public class StashedHandleView extends View {
* @param animate Whether to animate the change, or apply it immediately.
*/
public void updateHandleColor(boolean isRegionDark, boolean animate) {
if (mIsRegionDark != null && mIsRegionDark == isRegionDark) {
return;
}
int newColor = isRegionDark ? mStashedHandleLightColor : mStashedHandleDarkColor;
mIsRegionDark = isRegionDark;
if (mColorChangeAnim != null) {
mColorChangeAnim.cancel();
}
@@ -36,12 +36,13 @@ import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.RevealOutlineAnimation;
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.quickstep.NavHandle;
import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.wm.shell.shared.handles.RegionSamplingHelper;
import java.io.PrintWriter;
@@ -93,7 +94,6 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT
// States that affect whether region sampling is enabled or not
private boolean mIsStashed;
private boolean mIsLumaSamplingEnabled;
private boolean mIsAppTransitionPending;
private boolean mTaskbarHidden;
private float mTranslationYForSwipe;
@@ -118,8 +118,7 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT
mControllers = controllers;
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
Resources resources = mActivity.getResources();
if (mActivity.isPhoneGestureNavMode() || mActivity.isTinyTaskbar()
|| mActivity.isBubbleBarOnPhone()) {
if (mActivity.isPhoneGestureNavMode() || mActivity.isTinyTaskbar()) {
mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_phone_size);
mStashedHandleWidth =
resources.getDimensionPixelSize(R.dimen.taskbar_stashed_small_screen);
@@ -192,9 +191,7 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT
public void onDestroy() {
if (mRegionSamplingHelper != null) {
mRegionSamplingHelper.stopAndDestroy();
}
mRegionSamplingHelper.stopAndDestroy();
mRegionSamplingHelper = null;
}
@@ -212,11 +209,10 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT
* morphs into the size of where the taskbar icons will be.
*/
public Animator createRevealAnimToIsStashed(boolean isStashed) {
Rect visualBounds = mControllers.taskbarViewController
.getTransientTaskbarIconLayoutBounds();
Rect visualBounds = new Rect(mControllers.taskbarViewController.getIconLayoutBounds());
float startRadius = mStashedHandleRadius;
if (mActivity.isTransientTaskbar()) {
if (DisplayController.isTransientTaskbar(mActivity)) {
// Account for the full visual height of the transient taskbar.
int heightDiff = (mTaskbarSize - visualBounds.height()) / 2;
visualBounds.top -= heightDiff;
@@ -261,11 +257,6 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT
updateSamplingState();
}
public void setIsAppTransitionPending(boolean pending) {
mIsAppTransitionPending = pending;
updateSamplingState();
}
private void updateSamplingState() {
updateRegionSamplingWindowVisibility();
if (shouldSample()) {
@@ -277,7 +268,7 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT
}
private boolean shouldSample() {
return mIsStashed && mIsLumaSamplingEnabled && !mIsAppTransitionPending;
return mIsStashed && mIsLumaSamplingEnabled;
}
protected void updateStashedHandleHintScale() {
File diff suppressed because it is too large Load Diff
@@ -49,10 +49,6 @@ public class TaskbarAutohideSuspendController implements
public static final int FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR = 1 << 5;
// User has hovered the taskbar.
public static final int FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS = 1 << 6;
// User has multi instance window open.
public static final int FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN = 1 << 7;
// User has taskbar overflow open.
public static final int FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW = 1 << 8;
@IntDef(flag = true, value = {
FLAG_AUTOHIDE_SUSPEND_FULLSCREEN,
@@ -62,8 +58,6 @@ public class TaskbarAutohideSuspendController implements
FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER,
FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AutohideSuspendFlag {
@@ -144,10 +138,6 @@ public class TaskbarAutohideSuspendController implements
"FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER");
appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
"FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR");
appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN,
"FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN");
appendFlag(str, flags, FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW,
"FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW");
return str.toString();
}
}
@@ -30,6 +30,7 @@ import com.android.launcher3.Utilities.mapToRange
import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound
import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_PERSISTENT
import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_TRANSIENT
import com.android.launcher3.util.DisplayController
import kotlin.math.min
/** Helps draw the taskbar background, made up of a rectangle plus two inverted rounded corners. */
@@ -42,15 +43,13 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
private val maxPersistentTaskbarHeight =
context.persistentTaskbarDeviceProfile.taskbarHeight.toFloat()
var backgroundProgress =
if (context.isTransientTaskbar) {
if (DisplayController.isTransientTaskbar(context)) {
PINNING_TRANSIENT
} else {
PINNING_PERSISTENT
}
var isAnimatingPinning = false
var isAnimatingPersistentTaskbar = false
var isAnimatingTransientTaskbar = false
val paint = Paint()
private val strokePaint = Paint()
@@ -58,7 +57,6 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
var backgroundHeight = context.deviceProfile.taskbarHeight.toFloat()
var translationYForSwipe = 0f
var translationYForStash = 0f
var translationXForBubbleBar = 0f
private val transientBackgroundBounds = context.transientTaskbarBounds
@@ -68,8 +66,8 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
private var keyShadowDistance = 0f
private var bottomMargin = 0
private val fullCornerRadius: Float
private var cornerRadius = 0f
private val fullCornerRadius = context.cornerRadius.toFloat()
private var cornerRadius = fullCornerRadius
private var widthInsetPercentage = 0f
private val square = Path()
private val circle = Path()
@@ -99,17 +97,13 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
}
fullCornerRadius = context.cornerRadius.toFloat()
cornerRadius = fullCornerRadius
if (!context.isInDesktopMode()) {
setCornerRoundness(MAX_ROUNDNESS)
}
setCornerRoundness(DEFAULT_ROUNDNESS)
}
fun updateStashedHandleWidth(context: TaskbarActivityContext, res: Resources) {
stashedHandleWidth =
res.getDimensionPixelSize(
if (context.isPhoneMode || context.isTinyTaskbar || context.isBubbleBarOnPhone) {
if (context.isPhoneMode || context.isTinyTaskbar) {
R.dimen.taskbar_stashed_small_screen
} else {
R.dimen.taskbar_stashed_handle_width
@@ -123,7 +117,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
* @param cornerRoundness 0 has no round corner, 1 has complete round corner.
*/
fun setCornerRoundness(cornerRoundness: Float) {
if (context.isTransientTaskbar && !transientBackgroundBounds.isEmpty) {
if (DisplayController.isTransientTaskbar(context) && !transientBackgroundBounds.isEmpty) {
return
}
@@ -145,7 +139,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
/** Draws the background with the given paint and height, on the provided canvas. */
fun draw(canvas: Canvas) {
if (isInSetup) return
val isTransientTaskbar = context.isTransientTaskbar
val isTransientTaskbar = backgroundProgress == 0f
canvas.save()
if (!isTransientTaskbar || transientBackgroundBounds.isEmpty || isAnimatingPinning) {
drawPersistentBackground(canvas)
@@ -159,7 +153,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
}
private fun drawPersistentBackground(canvas: Canvas) {
if (isAnimatingPinning || isAnimatingPersistentTaskbar) {
if (isAnimatingPinning) {
val persistentTaskbarHeight = maxPersistentTaskbarHeight * backgroundProgress
canvas.translate(0f, canvas.height - persistentTaskbarHeight)
// Draw the background behind taskbar content.
@@ -182,20 +176,19 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
private fun drawTransientBackground(canvas: Canvas) {
val res = context.resources
val transientTaskbarHeight = maxTransientTaskbarHeight * (1f - backgroundProgress)
val isAnimating = isAnimatingPinning || isAnimatingTransientTaskbar
val heightProgressWhileAnimating =
if (isAnimating) transientTaskbarHeight else backgroundHeight
if (isAnimatingPinning) transientTaskbarHeight else backgroundHeight
var progress = heightProgressWhileAnimating / maxTransientTaskbarHeight
progress = Math.round(progress * 100f) / 100f
if (isAnimating) {
if (isAnimatingPinning) {
var scale = transientTaskbarHeight / maxTransientTaskbarHeight
scale = Math.round(scale * 100f) / 100f
bottomMargin =
mapRange(
scale,
0f,
res.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin).toFloat(),
res.getDimensionPixelSize(R.dimen.transient_taskbar_bottom_margin).toFloat()
)
.toInt()
shadowBlur =
@@ -234,7 +227,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
-mapRange(
1f - progress,
0f,
if (isAnimatingPinning) 0f else stashedHandleHeight / 2f,
if (isAnimatingPinning) 0f else stashedHandleHeight / 2f
)
// Draw shadow.
@@ -244,15 +237,15 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
shadowBlur,
0f,
keyShadowDistance,
setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha)),
setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
)
strokePaint.alpha = (paint.alpha * strokeAlpha) / 255
val currentTranslationX = translationXForBubbleBar * progress
lastDrawnTransientRect.set(
transientBackgroundBounds.left + halfWidthDelta + currentTranslationX,
transientBackgroundBounds.left + halfWidthDelta,
bottom - newBackgroundHeight,
transientBackgroundBounds.right - halfWidthDelta + currentTranslationX,
bottom,
transientBackgroundBounds.right - halfWidthDelta,
bottom
)
val horizontalInset = fullWidth * widthInsetPercentage
lastDrawnTransientRect.inset(horizontalInset, 0f)
@@ -270,7 +263,7 @@ class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
}
companion object {
const val MAX_ROUNDNESS = 1f
const val DEFAULT_ROUNDNESS = 1f
private const val DARK_THEME_STROKE_ALPHA = 51
private const val LIGHT_THEME_STROKE_ALPHA = 41
private const val DARK_THEME_SHADOW_ALPHA = 51f
@@ -15,7 +15,6 @@
*/
package com.android.launcher3.taskbar;
import android.animation.AnimatorSet;
import android.content.pm.ActivityInfo.Config;
import androidx.annotation.NonNull;
@@ -25,10 +24,8 @@ import androidx.annotation.VisibleForTesting;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
import com.android.launcher3.taskbar.bubbles.BubbleControllers;
import com.android.launcher3.taskbar.growth.NudgeController;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
import com.android.systemui.shared.rotation.RotationButtonController;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -67,8 +64,6 @@ public class TaskbarControllers {
public final KeyboardQuickSwitchController keyboardQuickSwitchController;
public final TaskbarPinningController taskbarPinningController;
public final Optional<BubbleControllers> bubbleControllers;
public final TaskbarDesktopModeController taskbarDesktopModeController;
public final NudgeController nudgeController;
@Nullable private LoggableTaskbarController[] mControllersToLog = null;
@Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null;
@@ -116,9 +111,7 @@ public class TaskbarControllers {
TaskbarEduTooltipController taskbarEduTooltipController,
KeyboardQuickSwitchController keyboardQuickSwitchController,
TaskbarPinningController taskbarPinningController,
Optional<BubbleControllers> bubbleControllers,
TaskbarDesktopModeController taskbarDesktopModeController,
NudgeController nudgeController) {
Optional<BubbleControllers> bubbleControllers) {
this.taskbarActivityContext = taskbarActivityContext;
this.taskbarDragController = taskbarDragController;
this.navButtonController = navButtonController;
@@ -145,8 +138,6 @@ public class TaskbarControllers {
this.keyboardQuickSwitchController = keyboardQuickSwitchController;
this.taskbarPinningController = taskbarPinningController;
this.bubbleControllers = bubbleControllers;
this.taskbarDesktopModeController = taskbarDesktopModeController;
this.nudgeController = nudgeController;
}
/**
@@ -154,15 +145,15 @@ public class TaskbarControllers {
* TaskbarControllers instance, but should be careful to only access things that were created
* in constructors for now, as some controllers may still be waiting for init().
*/
public void init(@NonNull TaskbarSharedState sharedState, AnimatorSet startAnimation) {
public void init(@NonNull TaskbarSharedState sharedState) {
mAreAllControllersInitialized = false;
mSharedState = sharedState;
taskbarDragController.init(this);
navbarButtonsViewController.init(this);
rotationButtonController.init();
taskbarDragLayerController.init(this, startAnimation);
taskbarViewController.init(this, startAnimation);
taskbarDragLayerController.init(this);
taskbarViewController.init(this);
taskbarScrimViewController.init(this);
taskbarUnfoldAnimationController.init(this);
taskbarKeyguardController.init(navbarButtonsViewController);
@@ -174,7 +165,6 @@ public class TaskbarControllers {
taskbarOverlayController.init(this);
taskbarAllAppsController.init(this, sharedState.allAppsVisible);
navButtonController.init(this);
bubbleControllers.ifPresent(controllers -> controllers.init(sharedState, this));
taskbarInsetsController.init(this);
voiceInteractionWindowController.init(this);
taskbarRecentAppsController.init(this);
@@ -182,8 +172,7 @@ public class TaskbarControllers {
taskbarEduTooltipController.init(this);
keyboardQuickSwitchController.init(this);
taskbarPinningController.init(this, mSharedState);
taskbarDesktopModeController.init(this, mSharedState);
nudgeController.init(this);
bubbleControllers.ifPresent(controllers -> controllers.init(this));
mControllersToLog = new LoggableTaskbarController[] {
taskbarDragController, navButtonController, navbarButtonsViewController,
@@ -194,29 +183,13 @@ public class TaskbarControllers {
voiceInteractionWindowController, taskbarRecentAppsController,
taskbarTranslationController, taskbarEduTooltipController,
keyboardQuickSwitchController, taskbarPinningController,
nudgeController
};
mBackgroundRendererControllers = new BackgroundRendererController[] {
taskbarDragLayerController, taskbarScrimViewController,
voiceInteractionWindowController
};
mCornerRoundness.updateValue(TaskbarBackgroundRenderer.DEFAULT_ROUNDNESS);
// TODO(b/401061748): get primary status from
// TaskbarDesktopModeController/DesktopVisibilityController.
if (taskbarDesktopModeController.isInDesktopModeAndNotInOverview(
taskbarActivityContext.getDisplayId())
|| !taskbarActivityContext.isPrimaryDisplay()) {
mCornerRoundness.value = taskbarDesktopModeController.getTaskbarCornerRoundness(
mSharedState.showCornerRadiusInDesktopMode);
} else {
mCornerRoundness.value = TaskbarBackgroundRenderer.MAX_ROUNDNESS;
}
updateCornerRoundness();
onPostInit();
}
@VisibleForTesting
public void onPostInit() {
mAreAllControllersInitialized = true;
for (Runnable postInitCallback : mPostInitCallbacks) {
postInitCallback.run();
@@ -232,17 +205,7 @@ public class TaskbarControllers {
uiController = newUiController;
uiController.init(this);
uiController.updateStateForSysuiFlags(mSharedState.sysuiStateFlags);
// if bubble controllers are present configure the UI controller
bubbleControllers.ifPresentOrElse(bubbleControllers -> {
BubbleBarLocation location =
bubbleControllers.bubbleBarViewController.getBubbleBarLocation();
boolean hiddenForBubbles =
bubbleControllers.bubbleBarViewController.isHiddenForNoBubbles();
if (!hiddenForBubbles) {
uiController.adjustHotseatForBubbleBar(/* isBubbleBarVisible= */ true);
}
uiController.onBubbleBarLocationUpdated(location);
}, () -> uiController.onBubbleBarLocationUpdated(null));
// Notify that the ui controller has changed
navbarButtonsViewController.onUiControllerChanged();
}
@@ -266,7 +229,6 @@ public class TaskbarControllers {
mAreAllControllersInitialized = false;
mSharedState = null;
taskbarDragController.onDestroy();
navbarButtonsViewController.onDestroy();
uiController.onDestroy();
rotationButtonController.onDestroy();
@@ -286,7 +248,7 @@ public class TaskbarControllers {
keyboardQuickSwitchController.onDestroy();
taskbarStashController.onDestroy();
bubbleControllers.ifPresent(controllers -> controllers.onDestroy());
taskbarDesktopModeController.onDestroy();
mControllersToLog = null;
mBackgroundRendererControllers = null;
}
@@ -320,11 +282,6 @@ public class TaskbarControllers {
}
uiController.dumpLogs(prefix + "\t", pw);
rotationButtonController.dumpLogs(prefix + "\t", pw);
if (bubbleControllers.isPresent()) {
bubbleControllers.get().dump(pw);
} else {
pw.println(String.format("%s\t%s", prefix, "Bubble controllers are empty."));
}
}
/**
@@ -350,7 +307,7 @@ public class TaskbarControllers {
return taskbarActivityContext;
}
public interface LoggableTaskbarController {
protected interface LoggableTaskbarController {
void dumpLogs(String prefix, PrintWriter pw);
}
@@ -31,20 +31,21 @@ import android.widget.LinearLayout
import android.widget.Switch
import androidx.core.view.postDelayed
import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.launcher3.Flags
import com.android.launcher3.R
import com.android.launcher3.popup.ArrowPopup
import com.android.launcher3.popup.RoundedArrowDrawable
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.Themes
import com.android.launcher3.views.ActivityContext
import kotlin.math.max
import kotlin.math.min
/** Popup view with arrow for taskbar pinning */
class TaskbarDividerPopupView<T : TaskbarActivityContext>
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
ArrowPopup<T>(context, attrs, defStyleAttr) {
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : ArrowPopup<T>(context, attrs, defStyleAttr) {
companion object {
private const val TAG = "TaskbarDividerPopupView"
private const val DIVIDER_POPUP_CLOSING_DELAY = 333L
@@ -54,33 +55,26 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
fun createAndPopulate(
view: View,
taskbarActivityContext: TaskbarActivityContext,
horizontalPosition: Float,
): TaskbarDividerPopupView<*> {
val taskMenuViewWithArrow =
taskbarActivityContext.layoutInflater.inflate(
R.layout.taskbar_divider_popup_menu,
taskbarActivityContext.dragLayer,
false,
false
) as TaskbarDividerPopupView<*>
return taskMenuViewWithArrow.populateForView(view, horizontalPosition)
return taskMenuViewWithArrow.populateForView(view)
}
}
private lateinit var dividerView: View
private var horizontalPosition = 0.0f
private val taskbarActivityContext: TaskbarActivityContext =
ActivityContext.lookupContext(context)
private val popupCornerRadius = Themes.getDialogCornerRadius(context)
private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
private val arrowHeight = resources.getDimension(R.dimen.popup_arrow_height)
private val arrowPointRadius = resources.getDimension(R.dimen.popup_arrow_corner_radius)
private val minPaddingFromScreenEdge =
resources.getDimension(R.dimen.taskbar_pinning_popup_menu_min_padding_from_screen_edge)
// TODO: add test for isTransientTaskbar & long presses divider and ensures the popup shows up.
private var alwaysShowTaskbarOn = !taskbarActivityContext.isTransientTaskbar
private var alwaysShowTaskbarOn = !DisplayController.isTransientTaskbar(context)
private var didPreferenceChange = false
private var verticalOffsetForPopupView =
resources.getDimensionPixelSize(R.dimen.taskbar_pinning_popup_menu_vertical_margin)
@@ -115,7 +109,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
}
alwaysShowTaskbarSwitch.setOnClickListener { view -> (view.parent as View).performClick() }
if (taskbarActivityContext.isGestureNav) {
if (ActivityContext.lookupContext<TaskbarActivityContext>(context).isGestureNav) {
taskbarSwitchOption.setOnClickListener {
alwaysShowTaskbarSwitch.isChecked = !alwaysShowTaskbarOn
onClickAlwaysShowTaskbarSwitchOption()
@@ -134,36 +128,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
/** Orient object as usual and then center object horizontally. */
override fun orientAboutObject() {
super.orientAboutObject()
x =
if (Flags.showTaskbarPinningPopupFromAnywhere()) {
val xForCenterAlignment = horizontalPosition - measuredWidth / 2f
val maxX = popupContainer.getWidth() - measuredWidth - minPaddingFromScreenEdge
when {
// Left-aligned popup and its arrow pointing to the event position if there is
// not enough space to center it.
xForCenterAlignment < minPaddingFromScreenEdge ->
max(
minPaddingFromScreenEdge,
horizontalPosition - mArrowOffsetHorizontal - mArrowWidth / 2,
)
// Right-aligned popup and its arrow pointing to the event position if there
// is not enough space to center it.
xForCenterAlignment > maxX ->
min(
horizontalPosition - measuredWidth +
mArrowOffsetHorizontal +
mArrowWidth / 2,
popupContainer.getWidth() - measuredWidth - minPaddingFromScreenEdge,
)
// Default alignment where the popup and its arrow are centered relative to the
// event position.
else -> xForCenterAlignment
}
} else {
mTempRect.centerX() - measuredWidth / 2f
}
x = mTempRect.centerX() - measuredWidth / 2f
}
override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
@@ -177,9 +142,8 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
return false
}
private fun populateForView(view: View, horizontalPosition: Float): TaskbarDividerPopupView<*> {
private fun populateForView(view: View): TaskbarDividerPopupView<*> {
dividerView = view
this@TaskbarDividerPopupView.horizontalPosition = horizontalPosition
tryUpdateBackground()
return this
}
@@ -205,31 +169,12 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
override fun addArrow() {
super.addArrow()
if (Flags.showTaskbarPinningPopupFromAnywhere()) {
mArrow.x =
min(
max(
minPaddingFromScreenEdge + mArrowOffsetHorizontal,
horizontalPosition - mArrowWidth / 2,
),
popupContainer.getWidth() -
minPaddingFromScreenEdge -
mArrowOffsetHorizontal -
mArrowWidth,
)
} else {
val location = IntArray(2)
popupContainer.getLocationInDragLayer(dividerView, location)
val dividerViewX = location[0].toFloat()
// Change arrow location to the middle of popup.
mArrow.x = (dividerViewX + dividerView.width / 2) - (mArrowWidth / 2)
}
// Change arrow location to the middle of popup.
mArrow.x = (dividerView.x + dividerView.width / 2) - (mArrowWidth / 2)
}
override fun updateArrowColor() {
if (Flags.showTaskbarPinningPopupFromAnywhere()) {
super.updateArrowColor()
} else if (!Gravity.isVertical(mGravity)) {
if (!Gravity.isVertical(mGravity)) {
mArrow.background =
RoundedArrowDrawable(
arrowWidth,
@@ -265,7 +210,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
/** Aligning the view pivot to center for animation. */
override fun setPivotForOpenCloseAnimation() {
pivotX = mArrow.x + mArrowWidth / 2 - x
pivotX = measuredWidth / 2f
pivotY = measuredHeight.toFloat()
}
@@ -279,13 +224,13 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
ObjectAnimator.ofFloat(
this,
TRANSLATION_Y,
*floatArrayOf(this.translationY, this.translationY + translateYValue),
*floatArrayOf(this.translationY, this.translationY + translateYValue)
)
val arrowTranslateY =
ObjectAnimator.ofFloat(
mArrow,
TRANSLATION_Y,
*floatArrayOf(mArrow.translationY, mArrow.translationY + translateYValue),
*floatArrayOf(mArrow.translationY, mArrow.translationY + translateYValue)
)
val animatorSet = AnimatorSet()
animatorSet.playTogether(alpha, arrowAlpha, translateY, arrowTranslateY)
@@ -295,7 +240,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
private fun getAnimatorOfFloat(
view: View,
property: Property<View, Float>,
vararg values: Float,
vararg values: Float
): Animator {
val animator: Animator = ObjectAnimator.ofFloat(view, property, *values)
animator.setDuration(DIVIDER_POPUP_CLOSING_ANIMATION_DURATION)
@@ -34,7 +34,6 @@ import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Point;
@@ -52,7 +51,6 @@ import android.view.ViewRootImpl;
import android.window.SurfaceSyncGroup;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.app.animation.Interpolators;
import com.android.internal.logging.InstanceId;
@@ -68,7 +66,6 @@ import com.android.launcher3.dragndrop.DragDriver;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.dragndrop.DragView;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.graphics.DragPreviewProvider;
import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
import com.android.launcher3.logging.StatsLogManager;
@@ -77,18 +74,18 @@ import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.views.BubbleTextHolder;
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.util.LogUtils;
import com.android.quickstep.util.MultiValueUpdateListener;
import com.android.quickstep.util.SingleTask;
import com.android.systemui.shared.recents.model.Task;
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.draganddrop.DragAndDropConstants;
import com.android.wm.shell.draganddrop.DragAndDropConstants;
import java.io.PrintWriter;
import java.util.Arrays;
@@ -96,7 +93,8 @@ import java.util.Collections;
import java.util.function.Predicate;
/**
* Handles long click on Taskbar items to start a system drag and drop operation.
* Handles long click on Taskbar items to start a system drag and drop
* operation.
*/
public class TaskbarDragController extends DragController<BaseTaskbarContext> implements
TaskbarControllers.LoggableTaskbarController {
@@ -116,7 +114,6 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
private int mRegistrationY;
private boolean mIsSystemDragInProgress;
private boolean mIsDropHandledByDropTarget;
// Animation for the drag shadow back into position after an unsuccessful drag
private ValueAnimator mReturnAnimator;
@@ -131,14 +128,6 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
public void init(TaskbarControllers controllers) {
mControllers = controllers;
mControllers.bubbleControllers.ifPresent(
c -> c.bubbleBarViewController.addBubbleBarDropTargets(this));
}
/** Called when the controller is destroyed. */
public void onDestroy() {
mControllers.bubbleControllers.ifPresent(
c -> c.bubbleBarViewController.removeBubbleBarDropTargets(this));
}
public void setDisallowGlobalDrag(boolean disallowGlobalDrag) {
@@ -150,8 +139,10 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
}
/**
* Attempts to start a system drag and drop operation for the given View, using its tag to
* Attempts to start a system drag and drop operation for the given View, using
* its tag to
* generate the ClipDescription and Intent.
*
* @return Whether {@link View#startDragAndDrop} started successfully.
*/
public boolean startDragOnLongClick(View view) {
@@ -193,9 +184,7 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
private DragView startInternalDrag(
BubbleTextView btv, @Nullable DragPreviewProvider dragPreviewProvider) {
// TODO(b/344038728): null check is only necessary because Recents doesn't use
// FastBitmapDrawable
float iconScale = btv.getIcon() == null ? 1f : btv.getIcon().getAnimatedScale();
float iconScale = btv.getIcon().getAnimatedScale();
// Clear the pressed state if necessary
btv.clearFocus();
@@ -203,7 +192,8 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
btv.clearPressedBackground();
final DragPreviewProvider previewProvider = dragPreviewProvider == null
? new DragPreviewProvider(btv) : dragPreviewProvider;
? new DragPreviewProvider(btv)
: dragPreviewProvider;
final Drawable drawable = previewProvider.createDrawable();
final float scale = previewProvider.getScaleAndPosition(drawable, mTempXY);
int dragLayerX = mTempXY[0];
@@ -215,13 +205,12 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
DragOptions dragOptions = new DragOptions();
// First, see if view is a search result that needs custom pre-drag conditions.
dragOptions.preDragCondition =
mControllers.taskbarAllAppsController.createPreDragConditionForSearch(btv);
dragOptions.preDragCondition = mControllers.taskbarAllAppsController.createPreDragConditionForSearch(btv);
if (dragOptions.preDragCondition == null) {
// See if view supports a popup container.
PopupContainerWithArrow<BaseTaskbarContext> popupContainer =
mControllers.taskbarPopupController.showForIcon(btv);
PopupContainerWithArrow<BaseTaskbarContext> popupContainer = mControllers.taskbarPopupController
.showForIcon(btv);
if (popupContainer != null) {
dragOptions.preDragCondition = popupContainer.createPreDragCondition(false);
}
@@ -261,9 +250,9 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
/* originalView = */ btv,
dragLayerX + dragOffset.x,
dragLayerY + dragOffset.y,
(View target, DropTarget.DragObject d, boolean success) ->
mIsDropHandledByDropTarget = success /* DragSource */,
btv.getTag() instanceof ItemInfo itemInfo ? itemInfo : null,
(View target, DropTarget.DragObject d, boolean success) -> {
} /* DragSource */,
(ItemInfo) btv.getTag(),
dragRect,
scale * iconScale,
scale,
@@ -303,23 +292,21 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
initialDragViewScale,
dragViewScaleOnDrop,
scalePx);
if (dragInfo != null) {
dragView.setItemInfo(dragInfo);
}
dragView.setItemInfo(dragInfo);
mDragObject.dragComplete = false;
mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop);
mDragDriver = DragDriver.create(this, mOptions, /* secondaryEventConsumer = */ ev -> {});
mDragDriver = DragDriver.create(this, mOptions, /* secondaryEventConsumer = */ ev -> {
});
if (!mOptions.isAccessibleDrag) {
mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
}
mDragObject.dragSource = source;
mDragObject.dragInfo = dragInfo;
mDragObject.originalDragInfo =
mDragObject.dragInfo != null ? mDragObject.dragInfo.makeShallowCopy() : null;
mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
if (mOptions.preDragCondition != null) {
dragView.setHasDragOffset(mOptions.preDragCondition.getDragOffset().x != 0
@@ -346,7 +333,8 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
/** Invoked when an animation running as part of pre-drag finishes. */
public void onPreDragAnimationEnd() {
// Drag might be cancelled during the DragView animation, so check mIsPreDrag again.
// Drag might be cancelled during the DragView animation, so check mIsPreDrag
// again.
if (mIsInPreDrag) {
callOnDragStart();
}
@@ -356,10 +344,12 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
protected void callOnDragStart() {
super.callOnDragStart();
// TODO(297921594) clean it up when taskbar to desktop drag is implemented.
DesktopVisibilityController desktopController = LauncherActivityInterface.INSTANCE
.getDesktopVisibilityController();
// Pre-drag has ended, start the global system drag.
if (mDisallowGlobalDrag
|| mControllers.taskbarDesktopModeController
.isInDesktopModeAndNotInOverview(mActivity.getDisplayId())) {
if (mDisallowGlobalDrag || (desktopController != null
&& desktopController.areDesktopTasksVisible())) {
AbstractFloatingView.closeAllOpenViewsExcept(mActivity, TYPE_TASKBAR_ALL_APPS);
return;
}
@@ -430,12 +420,9 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
item.user));
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, item.getIntent().getPackage());
intent.putExtra(Intent.EXTRA_SHORTCUT_ID, deepShortcutId);
ShortcutInfo shortcutInfo = ((WorkspaceItemInfo) item).getDeepShortcutInfo();
if (BubbleAnythingFlagHelper.enableCreateAnyBubble() && shortcutInfo != null) {
intent.putExtra(DragAndDropConstants.EXTRA_SHORTCUT_INFO, shortcutInfo);
}
} else if (item.itemType == ITEM_TYPE_SEARCH_ACTION) {
// TODO(b/289261756): Buggy behavior when split opposite to an existing search pane.
// TODO(b/289261756): Buggy behavior when split opposite to an existing search
// pane.
intent.putExtra(
ClipDescription.EXTRA_PENDING_INTENT,
PendingIntent.getActivityAsUser(
@@ -451,8 +438,8 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
null, item.user));
}
intent.putExtra(Intent.EXTRA_USER, item.user);
} else if (tag instanceof SingleTask singleTask) {
Task task = singleTask.getTask();
} else if (tag instanceof Task) {
Task task = (Task) tag;
clipDescription = new ClipDescription(task.titleDescription,
new String[] {
ClipDescription.MIMETYPE_APPLICATION_TASK
@@ -463,14 +450,14 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
}
if (clipDescription != null && intent != null) {
Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
LogUtils.getShellShareableInstanceId();
Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds = LogUtils
.getShellShareableInstanceId();
// Need to share the same InstanceId between launcher3 and WM Shell (internal).
InstanceId internalInstanceId = instanceIds.first;
com.android.launcher3.logging.InstanceId launcherInstanceId = instanceIds.second;
intent.putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, internalInstanceId);
if (mActivity.isTransientTaskbar()) {
if (DisplayController.isTransientTaskbar(mActivity)) {
// Tell WM Shell to ignore drag events in the provided transient taskbar region.
TaskbarDragLayer dragLayer = mControllers.taskbarActivityContext.getDragLayer();
int[] locationOnScreen = dragLayer.getLocationOnScreen();
@@ -510,8 +497,6 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
} else {
// This will take care of calling maybeOnDragEnd() after the animation
animateGlobalDragViewToOriginalPosition(btv, dragEvent);
//TODO(b/399678274): hide drop target in shell
notifyBubbleBarItemDragCanceled();
}
mActivity.getDragLayer().setOnDragListener(null);
@@ -531,61 +516,39 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
return mIsSystemDragInProgress;
}
@VisibleForTesting
private void maybeOnDragEnd() {
if (!isDragging()) {
((BubbleTextView) mDragObject.originalView).setIconDisabled(false);
mControllers.taskbarAutohideSuspendController.updateFlag(
TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING, false);
mActivity.onDragEnd();
// If an item is dropped on the bubble bar, the bubble bar handles the drop,
// so it should not collapse along with the taskbar.
boolean droppedOnBubbleBar = notifyBubbleBarItemDropped();
if (mReturnAnimator == null) {
// Upon successful drag, immediately stash taskbar.
// Note, this must be done last to ensure no AutohideSuspendFlags are active, as
// that will prevent us from stashing until the timeout.
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
/* stash = */ true,
/* shouldBubblesFollow = */ !droppedOnBubbleBar
);
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
mActivity.getStatsLogManager().logger().withItemInfo(mDragObject.dragInfo)
.log(LAUNCHER_APP_LAUNCH_DRAGDROP);
}
}
}
/**
* Exits the Bubble Bar drop target mode if applicable.
*
* @return {@code true} if drop target mode was active.
*/
private boolean notifyBubbleBarItemDropped() {
return mControllers.bubbleControllers.map(bc -> {
BubbleBarViewController bubbleBarViewController = bc.bubbleBarViewController;
boolean showingDropTarget = bubbleBarViewController.isShowingDropTarget();
if (showingDropTarget) {
bubbleBarViewController.onItemDragCompleted();
}
return showingDropTarget;
}).orElse(false);
}
private void notifyBubbleBarItemDragCanceled() {
mControllers.bubbleControllers.ifPresent(bc ->
bc.bubbleBarViewController.onItemDraggedOutsideBubbleBarDropZone());
}
@Override
protected void endDrag() {
if (mDisallowGlobalDrag && !mIsDropHandledByDropTarget) {
// We need to explicitly set deferDragViewCleanupPostAnimation to true here so the
// super call doesn't remove it from the drag layer before the animation completes.
if (mDisallowGlobalDrag) {
// We need to explicitly set deferDragViewCleanupPostAnimation to true here so
// the
// super call doesn't remove it from the drag layer before the animation
// completes.
// This variable gets set in to false in super.dispatchDropComplete() because it
// (rightfully so, perhaps) thinks this drag operation has failed, and does its own
// (rightfully so, perhaps) thinks this drag operation has failed, and does its
// own
// internal cleanup.
// Another way to approach this would be to make all of overview a drop target and
// accept the drop as successful and then run the setupReturnDragAnimator to simulate
// Another way to approach this would be to make all of overview a drop target
// and
// accept the drop as successful and then run the setupReturnDragAnimator to
// simulate
// drop failure to the user
mDragObject.deferDragViewCleanupPostAnimation = true;
@@ -670,7 +633,8 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
syncGroup.add(viewRoot, null /* runnable */);
syncGroup.addTransaction(transaction);
syncGroup.markSyncReady();
// Do this after maybeOnDragEnd(), because we use mReturnAnimator != null to imply
// Do this after maybeOnDragEnd(), because we use mReturnAnimator != null to
// imply
// the drag was canceled rather than successful.
mReturnAnimator = null;
}
@@ -691,7 +655,8 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
// We're dragging in taskbarAllApps, we don't have folders or shortcuts
return iconView;
}
// Since all apps closes when the drag starts, target the all apps button instead.
// Since all apps closes when the drag starts, target the all apps button
// instead.
return taskbarViewController.getAllAppsButtonView();
} else if (item.container >= 0) {
// Since folders close when the drag starts, target the folder icon instead.
@@ -711,8 +676,7 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
private static boolean isInSearchResultContainer(ItemInfo item) {
ContainerInfo containerInfo = item.getContainerInfo();
return containerInfo.getContainerCase() == EXTENDED_CONTAINERS
&& containerInfo.getExtendedContainers().getContainerCase()
== DEVICE_SEARCH_RESULT_CONTAINER;
&& containerInfo.getExtendedContainers().getContainerCase() == DEVICE_SEARCH_RESULT_CONTAINER;
}
private void setupReturnDragAnimator(float fromX, float fromY, View originalView,
@@ -741,6 +705,7 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
final FloatProp mDy = new FloatProp(fromY, toPosition[1], FAST_OUT_SLOW_IN);
final FloatProp mScale = new FloatProp(1f, toScale, FAST_OUT_SLOW_IN);
final FloatProp mAlpha = new FloatProp(1f, toAlpha, Interpolators.ACCELERATE_2);
@Override
public void onUpdate(float percent, boolean initOnly) {
animListener.updateDragShadow(mDx.value, mDy.value, mScale.value, mAlpha.value);
@@ -754,15 +719,19 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
@Override
protected float getX(MotionEvent ev) {
// We will resize to fill the screen while dragging, so use screen coordinates. This ensures
// we start at the correct position even though touch down is on the smaller DragLayer size.
// We will resize to fill the screen while dragging, so use screen coordinates.
// This ensures
// we start at the correct position even though touch down is on the smaller
// DragLayer size.
return ev.getRawX();
}
@Override
protected float getY(MotionEvent ev) {
// We will resize to fill the screen while dragging, so use screen coordinates. This ensures
// we start at the correct position even though touch down is on the smaller DragLayer size.
// We will resize to fill the screen while dragging, so use screen coordinates.
// This ensures
// we start at the correct position even though touch down is on the smaller
// DragLayer size.
return ev.getRawY();
}
@@ -782,11 +751,8 @@ public class TaskbarDragController extends DragController<BaseTaskbarContext> im
@Override
public void addDropTarget(DropTarget target) {
if (target instanceof Folder) {
// we need to ignore Folder.
return;
}
super.addDropTarget(target);
// No-op as Taskbar currently doesn't support any drop targets internally.
// Note: if we do add internal DropTargets, we'll still need to ignore Folder.
}
@Override
@@ -18,6 +18,7 @@ package com.android.launcher3.taskbar;
import static android.view.KeyEvent.ACTION_UP;
import static android.view.KeyEvent.KEYCODE_BACK;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
import android.content.Context;
@@ -42,6 +43,7 @@ import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Utilities;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.launcher3.views.BaseDragLayer;
@@ -100,16 +102,20 @@ public class TaskbarDragLayer extends BaseDragLayer<TaskbarActivityContext> {
public TaskbarDragLayer(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, 1 /* alphaChannelCount */);
mBackgroundRenderer = new TaskbarBackgroundRenderer(mContainer);
mBackgroundRenderer = new TaskbarBackgroundRenderer(mActivity);
mTaskbarBackgroundAlpha = new MultiPropertyFactory<>(this, BG_ALPHA, INDEX_COUNT,
(a, b) -> a * b, 1f);
mTaskbarBackgroundAlpha.get(INDEX_ALL_OTHER_STATES).setValue(0);
mTaskbarBackgroundAlpha.get(INDEX_STASH_ANIM).setValue(
enableScalingRevealHomeAnimation() && DisplayController.isTransientTaskbar(context)
? 0
: 1);
}
public void init(TaskbarDragLayerController.TaskbarDragLayerCallbacks callbacks) {
mControllerCallbacks = callbacks;
mBackgroundRenderer.updateStashedHandleWidth(mContainer, getResources());
mBackgroundRenderer.updateStashedHandleWidth(mActivity, getResources());
recreateControllers();
}
@@ -191,7 +197,6 @@ public class TaskbarDragLayer extends BaseDragLayer<TaskbarActivityContext> {
@Override
protected void dispatchDraw(Canvas canvas) {
if (mContainer.isDestroyed()) return;
float backgroundHeight = mControllerCallbacks.getTaskbarBackgroundHeight()
* (1f - mTaskbarBackgroundOffset);
mBackgroundRenderer.setBackgroundHeight(backgroundHeight);
@@ -260,11 +265,6 @@ public class TaskbarDragLayer extends BaseDragLayer<TaskbarActivityContext> {
invalidate();
}
protected void setBackgroundTranslationXForBubbleBar(float translationX) {
mBackgroundRenderer.setTranslationXForBubbleBar(translationX);
invalidate();
}
/** Returns the bounds in DragLayer coordinates of where the transient background was drawn. */
protected RectF getLastDrawnTransientRect() {
return mBackgroundRenderer.getLastDrawnTransientRect();
@@ -273,7 +273,6 @@ public class TaskbarDragLayer extends BaseDragLayer<TaskbarActivityContext> {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
mControllerCallbacks.onDispatchTouchEvent(ev);
return super.dispatchTouchEvent(ev);
}
@@ -281,7 +280,7 @@ public class TaskbarDragLayer extends BaseDragLayer<TaskbarActivityContext> {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK) {
AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer);
AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
if (topView != null && topView.canHandleBack()) {
topView.onBackInvoked();
// Handled by the floating view.
@@ -291,21 +290,6 @@ public class TaskbarDragLayer extends BaseDragLayer<TaskbarActivityContext> {
return super.dispatchKeyEvent(event);
}
/**
* Sets animation boolean when only animating persistent taskbar.
*/
public void setIsAnimatingPersistentTaskbarBackground(boolean animatingPersistentTaskbarBg) {
mBackgroundRenderer.setAnimatingPersistentTaskbar(animatingPersistentTaskbarBg);
}
/**
* Sets animation boolean when only animating transient taskbar.
*/
public void setIsAnimatingTransientTaskbarBackground(boolean animatingTransientTaskbarBg) {
mBackgroundRenderer.setAnimatingTransientTaskbar(animatingTransientTaskbarBg);
}
/**
* Sets the width percentage to inset the transient taskbar's background from the left and from
* the right.
@@ -15,25 +15,21 @@
*/
package com.android.launcher3.taskbar;
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT;
import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.SystemProperties;
import android.view.MotionEvent;
import android.view.ViewTreeObserver;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.util.DimensionUtils;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.launcher3.util.TouchController;
@@ -61,8 +57,6 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa
private final AnimatedFloat mImeBgTaskbar = new AnimatedFloat(this::updateBackgroundAlpha);
private final AnimatedFloat mAssistantBgTaskbar = new AnimatedFloat(
this::updateBackgroundAlpha);
private final AnimatedFloat mBgTaskbarRecreate = new AnimatedFloat(
this::updateBackgroundAlpha);
// Used to hide our background color when someone else (e.g. ScrimView) is handling it.
private final AnimatedFloat mBgOverride = new AnimatedFloat(this::updateBackgroundAlpha);
@@ -93,10 +87,7 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa
mFolderMargin = resources.getDimensionPixelSize(R.dimen.taskbar_folder_margin);
}
/**
* Init of taskbar drag layer controller
*/
public void init(TaskbarControllers controllers, AnimatorSet startAnimation) {
public void init(TaskbarControllers controllers) {
mControllers = controllers;
mTaskbarStashViaTouchController = new TaskbarStashViaTouchController(mControllers);
mTaskbarDragLayer.init(new TaskbarDragLayerCallbacks());
@@ -104,43 +95,15 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa
mOnBackgroundNavButtonColorIntensity = mControllers.navbarButtonsViewController
.getOnTaskbarBackgroundNavButtonColorOverride();
if (startAnimation != null) {
// set taskbar background render animation boolean
if (mActivity.isTransientTaskbar()) {
mTaskbarDragLayer.setIsAnimatingTransientTaskbarBackground(true);
} else {
mTaskbarDragLayer.setIsAnimatingPersistentTaskbarBackground(true);
}
float desiredValue = mActivity.isTransientTaskbar()
? PINNING_TRANSIENT
: PINNING_PERSISTENT;
float nonDesiredvalue =
!mActivity.isTransientTaskbar() ? PINNING_TRANSIENT : PINNING_PERSISTENT;
ObjectAnimator objectAnimator = mTaskbarBackgroundProgress.animateToValue(
nonDesiredvalue, desiredValue);
objectAnimator.setInterpolator(EMPHASIZED);
startAnimation.play(objectAnimator);
startAnimation.addListener(AnimatorListeners.forEndCallback(()-> {
// reset taskbar background render animation boolean
mTaskbarDragLayer.setIsAnimatingPersistentTaskbarBackground(false);
mTaskbarDragLayer.setIsAnimatingTransientTaskbarBackground(false);
}));
} else {
mTaskbarBackgroundProgress.updateValue(
mActivity.isTransientTaskbar() ? PINNING_TRANSIENT : PINNING_PERSISTENT);
}
mTaskbarBackgroundProgress.updateValue(DisplayController.isTransientTaskbar(mActivity)
? PINNING_TRANSIENT
: PINNING_PERSISTENT);
mBgTaskbar.value = 1;
mKeyguardBgTaskbar.value = 1;
mNotificationShadeBgTaskbar.value = 1;
mImeBgTaskbar.value = 1;
mAssistantBgTaskbar.value = 1;
mBgTaskbarRecreate.value = 1;
mBgOverride.value = 1;
updateBackgroundAlpha();
@@ -148,13 +111,6 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa
updateTaskbarAlpha();
}
/**
* Called when destroying Taskbar with animation.
*/
public void onDestroyAnimation(AnimatorSet animatorSet) {
animatorSet.play(mBgTaskbarRecreate.animateToValue(0f));
}
public void onDestroy() {
mTaskbarDragLayer.onDestroy();
}
@@ -215,14 +171,10 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa
}
private void updateBackgroundAlpha() {
if (mActivity.isPhoneMode() || mActivity.isDestroyed()) {
return;
}
final float bgNavbar = mBgNavbar.value;
final float bgTaskbar = mBgTaskbar.value * mKeyguardBgTaskbar.value
* mNotificationShadeBgTaskbar.value * mImeBgTaskbar.value
* mAssistantBgTaskbar.value * mBgTaskbarRecreate.value;
* mAssistantBgTaskbar.value;
mLastSetBackgroundAlpha = mBgOverride.value * Math.max(bgNavbar, bgTaskbar);
mBackgroundRendererAlpha.setValue(mLastSetBackgroundAlpha);
@@ -240,13 +192,6 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa
mTaskbarDragLayer.setBackgroundTranslationYForSwipe(transY);
}
/**
* Sets the translation of the background for the bubble bar.
*/
public void setTranslationXForBubbleBar(float transX) {
mTaskbarDragLayer.setBackgroundTranslationXForBubbleBar(transX);
}
/**
* Sets the translation of the background during the spring on stash animation.
*/
@@ -309,7 +254,6 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa
pw.println(prefix + "\t\tmNotificationShadeBgTaskbar=" + mNotificationShadeBgTaskbar.value);
pw.println(prefix + "\t\tmImeBgTaskbar=" + mImeBgTaskbar.value);
pw.println(prefix + "\t\tmAssistantBgTaskbar=" + mAssistantBgTaskbar.value);
pw.println(prefix + "\t\tmBgTaskbarRecreate=" + mBgTaskbarRecreate.value);
}
/**
@@ -347,7 +291,7 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa
if (mActivity.isPhoneMode()) {
Resources resources = mActivity.getResources();
Point taskbarDimensions = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile,
resources, true /* isPhoneMode */, mActivity.isGestureNav());
resources, true /* isPhoneMode */);
return taskbarDimensions.y == -1 ?
deviceProfile.getDisplayInfo().currentSize.y :
taskbarDimensions.y;
@@ -377,15 +321,5 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa
}
mControllers.taskbarInsetsController.drawDebugTouchableRegionBounds(canvas);
}
/** Handles any touch event before it is dispatched to the rest of TaskbarDragLayer. */
public void onDispatchTouchEvent(MotionEvent ev) {
if (mActivity.isThreeButtonNav() && ev.getAction() == MotionEvent.ACTION_OUTSIDE
&& mControllers.uiController.isAnimatingToHotseat()) {
// When touching during animation to home, jump to the end so Hotseat can handle
// the touch. (Gesture Navigation handles this in AbsSwipeUpHandler.)
mControllers.uiController.endAnimationToHotseat();
}
}
}
}
@@ -29,6 +29,7 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.animation.Interpolator
import android.window.OnBackInvokedDispatcher
import androidx.core.view.updateLayoutParams
import app.lawnchair.theme.color.tokens.ColorTokens
import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.app.animation.Interpolators.STANDARD
@@ -39,20 +40,22 @@ import com.android.launcher3.popup.RoundedArrowDrawable
import com.android.launcher3.util.Themes
import com.android.launcher3.views.ActivityContext
import app.lawnchair.theme.color.tokens.ColorTokens
private const val ENTER_DURATION_MS = 300L
private const val EXIT_DURATION_MS = 150L
/** Floating tooltip for Taskbar education. */
class TaskbarEduTooltip
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
AbstractFloatingView(context, attrs, defStyleAttr) {
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : AbstractFloatingView(context, attrs, defStyleAttr) {
private val activityContext: ActivityContext = ActivityContext.lookupContext(context)
private val backgroundColor = ColorTokens.SurfaceBrightColor.resolveColor(getContext())
private val backgroundColor =
ColorTokens.SurfaceBrightColor.resolveColor(getContext())
private val tooltipCornerRadius = Themes.getDialogCornerRadius(context)
private val arrowWidth = resources.getDimension(R.dimen.popup_arrow_width)
@@ -99,8 +102,8 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
override fun onFinishInflate() {
super.onFinishInflate()
content = requireViewById(R.id.content)
arrow = requireViewById(R.id.arrow)
content = requireViewById<android.widget.FrameLayout>(R.id.content)
arrow = requireViewById<View>(R.id.arrow)
arrow.background =
RoundedArrowDrawable(
arrowWidth,
@@ -148,7 +151,6 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
override fun onAttachedToWindow() {
super.onAttachedToWindow()
// Lawnchair-TODO-Merge: This was disabled but enabled in 16r2, this function likely disable the gesture system during edu
// findOnBackInvokedDispatcher()
// ?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this)
}
@@ -33,26 +33,22 @@ import android.view.accessibility.AccessibilityNodeInfo
import android.widget.TextView
import androidx.annotation.IntDef
import androidx.annotation.LayoutRes
import androidx.annotation.VisibleForTesting
import androidx.core.text.HtmlCompat
import androidx.core.view.updateLayoutParams
import com.airbnb.lottie.LottieAnimationView
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.R
import com.android.launcher3.RemoveAnimationSettingsTracker
import com.android.launcher3.Utilities
import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN
import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP
import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN
import com.android.launcher3.util.ResourceBasedOverride
import com.android.launcher3.views.ActivityContext
import com.android.launcher3.views.BaseDragLayer
import com.android.quickstep.util.ContextualSearchInvoker
import com.android.quickstep.util.LottieAnimationColorUtils
import com.android.wm.shell.shared.TypefaceUtils
import com.android.wm.shell.shared.TypefaceUtils.FontFamily
import java.io.PrintWriter
/** First EDU step for swiping up to show transient Taskbar. */
@@ -83,11 +79,7 @@ open class TaskbarEduTooltipController(context: Context) :
ResourceBasedOverride, LoggableTaskbarController {
protected val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
open val shouldShowSearchEdu: Boolean
get() =
ContextualSearchInvoker(activityContext)
.runContextualSearchInvocationChecksAndLogFailures()
open val shouldShowSearchEdu = false
private val isTooltipEnabled: Boolean
get() {
return !Utilities.isRunningInTestHarness() &&
@@ -95,7 +87,7 @@ open class TaskbarEduTooltipController(context: Context) :
!activityContext.isTinyTaskbar
}
val isTooltipOpen: Boolean
private val isOpen: Boolean
get() = tooltip?.isOpen ?: false
val isBeforeTooltipFeaturesStep: Boolean
@@ -104,8 +96,7 @@ open class TaskbarEduTooltipController(context: Context) :
private lateinit var controllers: TaskbarControllers
// Keep track of whether the user has seen the Search Edu
@VisibleForTesting
var userHasSeenSearchEdu: Boolean
private var userHasSeenSearchEdu: Boolean
get() {
return TASKBAR_SEARCH_EDU_SEEN.get(activityContext)
}
@@ -130,31 +121,11 @@ open class TaskbarEduTooltipController(context: Context) :
activityContext.dragLayer.post { maybeShowSearchEdu() }
}
/**
* Turns off auto play of lottie animations if user has opted to remove animation else attaches
* click listener to allow user to play or pause animations.
*/
fun handleEduAnimations(animationViews: List<LottieAnimationView>) {
for (animationView in animationViews) {
if (
RemoveAnimationSettingsTracker.INSTANCE.get(animationView.context)
.isRemoveAnimationEnabled()
) {
animationView.pauseAnimation()
} else {
animationView.setOnClickListener {
if (animationView.isAnimating) animationView.pauseAnimation()
else animationView.playAnimation()
}
}
}
}
/** Shows swipe EDU tooltip if it is the current [tooltipStep]. */
fun maybeShowSwipeEdu() {
if (
!isTooltipEnabled ||
!activityContext.isTransientTaskbar ||
!DisplayController.isTransientTaskbar(activityContext) ||
tooltipStep > TOOLTIP_STEP_SWIPE
) {
return
@@ -163,13 +134,7 @@ open class TaskbarEduTooltipController(context: Context) :
tooltipStep = TOOLTIP_STEP_FEATURES
inflateTooltip(R.layout.taskbar_edu_swipe)
tooltip?.run {
TypefaceUtils.setTypeface(
requireViewById(R.id.taskbar_edu_title),
FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
)
val swipeAnimation = requireViewById<LottieAnimationView>(R.id.swipe_animation)
swipeAnimation.supportLightTheme()
handleEduAnimations(listOf(swipeAnimation))
requireViewById<LottieAnimationView>(R.id.swipe_animation).supportLightTheme()
show()
}
}
@@ -198,8 +163,7 @@ open class TaskbarEduTooltipController(context: Context) :
splitscreenAnim.supportLightTheme()
suggestionsAnim.supportLightTheme()
pinningAnim.supportLightTheme()
handleEduAnimations(listOf(splitscreenAnim, suggestionsAnim, pinningAnim))
if (activityContext.isTransientTaskbar) {
if (DisplayController.isTransientTaskbar(activityContext)) {
splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_transient)
suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_transient)
pinningEdu.visibility = if (enableTaskbarPinning()) VISIBLE else GONE
@@ -209,27 +173,10 @@ open class TaskbarEduTooltipController(context: Context) :
pinningEdu.visibility = GONE
}
TypefaceUtils.setTypeface(
requireViewById(R.id.taskbar_edu_title),
FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
)
TypefaceUtils.setTypeface(
requireViewById(R.id.splitscreen_text),
FontFamily.GSF_BODY_MEDIUM,
)
TypefaceUtils.setTypeface(
requireViewById(R.id.suggestions_text),
FontFamily.GSF_BODY_MEDIUM,
)
TypefaceUtils.setTypeface(
requireViewById(R.id.pinning_text),
FontFamily.GSF_BODY_MEDIUM,
)
// Set up layout parameters.
content.updateLayoutParams { width = MATCH_PARENT }
updateLayoutParams<MarginLayoutParams> {
if (activityContext.isTransientTaskbar) {
if (DisplayController.isTransientTaskbar(activityContext)) {
width =
resources.getDimensionPixelSize(
if (enableTaskbarPinning())
@@ -262,7 +209,7 @@ open class TaskbarEduTooltipController(context: Context) :
// for the original 2 edu steps) as a proxy to needing to show the separate pinning edu
if (
!enableTaskbarPinning() ||
!activityContext.isTransientTaskbar ||
!DisplayController.isTransientTaskbar(activityContext) ||
!isTooltipEnabled ||
tooltipStep > TOOLTIP_STEP_PINNING ||
tooltipStep < TOOLTIP_STEP_FEATURES
@@ -274,21 +221,11 @@ open class TaskbarEduTooltipController(context: Context) :
tooltip?.run {
allowTouchDismissal = true
TypefaceUtils.setTypeface(
requireViewById(R.id.taskbar_edu_title),
FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
)
TypefaceUtils.setTypeface(
requireViewById(R.id.pinning_text),
FontFamily.GSF_BODY_MEDIUM,
)
requireViewById<LottieAnimationView>(R.id.standalone_pinning_animation)
.supportLightTheme()
val pinningAnim =
requireViewById<LottieAnimationView>(R.id.standalone_pinning_animation)
pinningAnim.supportLightTheme()
handleEduAnimations(listOf(pinningAnim))
updateLayoutParams<BaseDragLayer.LayoutParams> {
if (activityContext.isTransientTaskbar) {
if (DisplayController.isTransientTaskbar(activityContext)) {
bottomMargin += activityContext.deviceProfile.taskbarHeight
}
// Unlike other tooltips, we want to align with taskbar divider rather than center.
@@ -318,11 +255,10 @@ open class TaskbarEduTooltipController(context: Context) :
fun maybeShowSearchEdu() {
if (
!enableTaskbarPinning() ||
!activityContext.isPinnedTaskbar ||
!DisplayController.isPinnedTaskbar(activityContext) ||
!isTooltipEnabled ||
!shouldShowSearchEdu ||
userHasSeenSearchEdu ||
!controllers.taskbarStashController.isTaskbarVisibleAndNotStashing
userHasSeenSearchEdu
) {
return
}
@@ -330,20 +266,11 @@ open class TaskbarEduTooltipController(context: Context) :
inflateTooltip(R.layout.taskbar_edu_search)
tooltip?.run {
allowTouchDismissal = true
val searchEdu = requireViewById<LottieAnimationView>(R.id.search_edu_animation)
searchEdu.supportLightTheme()
handleEduAnimations(listOf(searchEdu))
requireViewById<LottieAnimationView>(R.id.search_edu_animation).supportLightTheme()
val eduSubtitle: TextView = requireViewById(R.id.search_edu_text)
TypefaceUtils.setTypeface(
requireViewById(R.id.taskbar_edu_title),
FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
)
TypefaceUtils.setTypeface(eduSubtitle, FontFamily.GSF_BODY_SMALL)
showDisclosureText(eduSubtitle)
updateLayoutParams<BaseDragLayer.LayoutParams> {
if (activityContext.isTransientTaskbar) {
if (DisplayController.isTransientTaskbar(activityContext)) {
bottomMargin += activityContext.deviceProfile.taskbarHeight
}
// Unlike other tooltips, we want to align with the all apps button rather than
@@ -422,19 +349,19 @@ open class TaskbarEduTooltipController(context: Context) :
overlayContext.layoutInflater.inflate(
R.layout.taskbar_edu_tooltip,
overlayContext.dragLayer,
false,
false
) as TaskbarEduTooltip
controllers.taskbarAutohideSuspendController.updateFlag(
FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
true,
true
)
tooltip.onCloseCallback = {
this.tooltip = null
controllers.taskbarAutohideSuspendController.updateFlag(
FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
false,
false
)
controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
}
@@ -449,7 +376,7 @@ open class TaskbarEduTooltipController(context: Context) :
override fun performAccessibilityAction(
host: View,
action: Int,
args: Bundle?,
args: Bundle?
): Boolean {
if (action == R.id.close) {
hide()
@@ -467,13 +394,13 @@ open class TaskbarEduTooltipController(context: Context) :
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfo,
info: AccessibilityNodeInfo
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.addAction(
AccessibilityNodeInfo.AccessibilityAction(
R.id.close,
host.context?.getText(R.string.taskbar_edu_close),
host.context?.getText(R.string.taskbar_edu_close)
)
)
}
@@ -482,7 +409,7 @@ open class TaskbarEduTooltipController(context: Context) :
override fun dumpLogs(prefix: String?, pw: PrintWriter?) {
pw?.println(prefix + "TaskbarEduTooltipController:")
pw?.println("$prefix\tisTooltipEnabled=$isTooltipEnabled")
pw?.println("$prefix\tisOpen=$isTooltipOpen")
pw?.println("$prefix\tisOpen=$isOpen")
pw?.println("$prefix\ttooltipStep=$tooltipStep")
}
@@ -492,7 +419,7 @@ open class TaskbarEduTooltipController(context: Context) :
return ResourceBasedOverride.Overrides.getObject(
TaskbarEduTooltipController::class.java,
context,
R.string.taskbar_edu_tooltip_controller_class,
R.string.taskbar_edu_tooltip_controller_class
)
}
}
@@ -85,9 +85,6 @@ public class TaskbarForceVisibleImmersiveController implements TouchController {
/** Update values tracked via sysui flags. */
public void updateSysuiFlags(@SystemUiStateFlags long sysuiFlags) {
if (mContext.isPhoneMode()) {
return;
}
mIsImmersiveMode = (sysuiFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) == 0;
if (mContext.isNavBarForceVisible()) {
if (mIsImmersiveMode) {
@@ -108,10 +105,8 @@ public class TaskbarForceVisibleImmersiveController implements TouchController {
/** Clean up animations. */
public void onDestroy() {
startIconUndimming();
if (mControllers != null) {
mControllers.navbarButtonsViewController.setHomeButtonAccessibilityDelegate(null);
mControllers.navbarButtonsViewController.setBackButtonAccessibilityDelegate(null);
}
mControllers.navbarButtonsViewController.setHomeButtonAccessibilityDelegate(null);
mControllers.navbarButtonsViewController.setBackButtonAccessibilityDelegate(null);
}
private void startIconUndimming() {
@@ -18,23 +18,29 @@ package com.android.launcher3.taskbar;
import static android.view.MotionEvent.ACTION_HOVER_ENTER;
import static android.view.MotionEvent.ACTION_HOVER_EXIT;
import static android.view.View.ALPHA;
import static android.view.View.SCALE_Y;
import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT;
import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP;
import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER;
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_EXCEPT_ON_BOARD_POPUP;
import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
import static com.android.launcher3.views.ArrowTipView.TEXT_ALPHA;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.view.ContextThemeWrapper;
import android.view.MotionEvent;
import android.view.View;
import com.android.app.animation.Interpolators;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.views.ArrowTipView;
@@ -42,16 +48,19 @@ import com.android.launcher3.views.ArrowTipView;
* Controls showing a tooltip in the taskbar above each icon when it is hovered.
*/
public class TaskbarHoverToolTipController implements View.OnHoverListener {
// Short duration to reveal tooltip, as it is positioned in the x/y via a post() call in
// parallel with the open animation. An instant animation could show in the wrong location.
private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 15;
private static final int HOVER_TOOL_TIP_REVEAL_DURATION = 250;
private static final int HOVER_TOOL_TIP_EXIT_DURATION = 150;
private final Handler mHoverToolTipHandler = new Handler(Looper.getMainLooper());
private final Runnable mRevealHoverToolTipRunnable = this::revealHoverToolTip;
private final Runnable mHideHoverToolTipRunnable = this::hideHoverToolTip;
private final TaskbarActivityContext mActivity;
private final TaskbarView mTaskbarView;
private final View mHoverView;
private final ArrowTipView mHoverToolTipView;
private final String mToolTipText;
private final int mYOffset;
public TaskbarHoverToolTipController(TaskbarActivityContext activity, TaskbarView taskbarView,
View hoverView) {
@@ -64,8 +73,6 @@ public class TaskbarHoverToolTipController implements View.OnHoverListener {
} else if (mHoverView instanceof FolderIcon
&& ((FolderIcon) mHoverView).mInfo.title != null) {
mToolTipText = ((FolderIcon) mHoverView).mInfo.title.toString();
} else if (mHoverView instanceof AppPairIcon) {
mToolTipText = ((AppPairIcon) mHoverView).getTitleTextView().getText().toString();
} else {
mToolTipText = null;
}
@@ -80,55 +87,90 @@ public class TaskbarHoverToolTipController implements View.OnHoverListener {
R.dimen.taskbar_tooltip_horizontal_padding);
mHoverToolTipView.findViewById(R.id.text).setPadding(horizontalPadding, verticalPadding,
horizontalPadding, verticalPadding);
mHoverToolTipView.setAlpha(0);
mYOffset = arrowContextWrapper.getResources().getDimensionPixelSize(
R.dimen.taskbar_tooltip_y_offset);
AnimatorSet hoverCloseAnimator = new AnimatorSet();
ObjectAnimator textCloseAnimator = ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 0);
textCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0, 0.33f));
ObjectAnimator alphaCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 0);
alphaCloseAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.33f, 0.66f));
ObjectAnimator scaleCloseAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 0);
scaleCloseAnimator.setInterpolator(Interpolators.STANDARD);
hoverCloseAnimator.playTogether(
textCloseAnimator,
alphaCloseAnimator,
scaleCloseAnimator);
hoverCloseAnimator.setStartDelay(0);
hoverCloseAnimator.setDuration(HOVER_TOOL_TIP_EXIT_DURATION);
mHoverToolTipView.setCustomCloseAnimation(hoverCloseAnimator);
AnimatorSet hoverOpenAnimator = new AnimatorSet();
ObjectAnimator textOpenAnimator =
ObjectAnimator.ofInt(mHoverToolTipView, TEXT_ALPHA, 0, 255);
textOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0.15f, 0.75f));
ObjectAnimator scaleOpenAnimator =
ObjectAnimator.ofFloat(mHoverToolTipView, SCALE_Y, 0f, 1f);
scaleOpenAnimator.setInterpolator(Interpolators.EMPHASIZED);
ObjectAnimator alphaOpenAnimator = ObjectAnimator.ofFloat(mHoverToolTipView, ALPHA, 0f, 1f);
hoverOpenAnimator.play(alphaOpenAnimator);
alphaOpenAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0f, 0.33f));
hoverOpenAnimator.playTogether(
scaleOpenAnimator,
textOpenAnimator,
alphaOpenAnimator);
hoverOpenAnimator.setDuration(HOVER_TOOL_TIP_REVEAL_DURATION);
mHoverToolTipView.setCustomOpenAnimation(hoverOpenAnimator);
mHoverToolTipView.addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
mHoverToolTipView.setPivotY(bottom);
mHoverToolTipView.setY(mTaskbarView.getTop() - mYOffset - (bottom - top));
mHoverToolTipView.setY(mTaskbarView.getTop() - (bottom - top));
});
}
@Override
public boolean onHover(View v, MotionEvent event) {
boolean isAnyOtherFloatingViewOpen =
AbstractFloatingView.hasOpenView(mActivity, TYPE_ALL_EXCEPT_ON_BOARD_POPUP);
if (isAnyOtherFloatingViewOpen) {
mHoverToolTipHandler.removeCallbacksAndMessages(null);
}
// If hover leaves a taskbar icon animate the tooltip closed.
if (event.getAction() == ACTION_HOVER_EXIT) {
mHoverToolTipView.close(/* animate= */ false);
startHideHoverToolTip();
mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, false);
} else if (event.getAction() == ACTION_HOVER_ENTER) {
maybeRevealHoverToolTip();
return true;
} else if (!isAnyOtherFloatingViewOpen && event.getAction() == ACTION_HOVER_ENTER) {
// If hovering above a taskbar icon starts, animate the tooltip open. Do not
// reveal if any floating views such as folders or edu pop-ups are open.
startRevealHoverToolTip();
mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, true);
return true;
}
return false;
}
private void maybeRevealHoverToolTip() {
private void startRevealHoverToolTip() {
mHoverToolTipHandler.post(mRevealHoverToolTipRunnable);
}
private void revealHoverToolTip() {
if (mHoverView == null || mToolTipText == null) {
return;
}
// Do not show tooltip if taskbar icons are transitioning to hotseat.
if (mActivity.isIconAlignedWithHotseat()) {
return;
}
if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) {
return;
}
// Do not reveal if floating views such as folders or app pop-ups are open,
// as these views will overlap and not look great.
if (AbstractFloatingView.hasOpenView(mActivity, TYPE_FOLDER | TYPE_ACTION_POPUP)) {
return;
}
Rect iconViewBounds = Utilities.getViewBounds(mHoverView);
mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(),
mTaskbarView.getTop() - mYOffset, /* shouldAutoClose= */ false);
mTaskbarView.getTop(), /* shouldAutoClose= */ false);
}
private void startHideHoverToolTip() {
int accessibilityHideTimeout = AccessibilityManagerCompat.getRecommendedTimeoutMillis(
mActivity, /* originalTimeout= */ 0, FLAG_CONTENT_TEXT);
mHoverToolTipHandler.postDelayed(mHideHoverToolTipRunnable, accessibilityHideTimeout);
}
private void hideHoverToolTip() {
mHoverToolTipView.close(/* animate = */ true);
}
}
@@ -21,6 +21,7 @@ import android.graphics.Insets
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Region
import android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR
import android.os.Binder
import android.os.IBinder
import android.view.DisplayInfo
@@ -51,9 +52,11 @@ import com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATI
import com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate
import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
import com.android.launcher3.testing.shared.ResourceUtils
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.Executors
import java.io.PrintWriter
import kotlin.jvm.optionals.getOrNull
import kotlin.math.max
/** Handles the insets that Taskbar provides to underlying apps and the IME. */
class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTaskbarController {
@@ -61,10 +64,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
companion object {
private const val INDEX_LEFT = 0
private const val INDEX_RIGHT = 1
private fun Region.addBoundsToRegion(bounds: Rect?) {
bounds?.let { op(it, Region.Op.UNION) }
}
const val FLAG_INSETS_ROUNDED_CORNER = 1 shl 1
}
/** The bottom insets taskbar provides to the IME when IME is visible. */
@@ -80,7 +80,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
context.mainThreadHandler,
Executors.UI_HELPER_EXECUTOR.handler,
context,
this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged,
this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged
)
private val debugTouchableRegion = DebugTouchableRegion()
@@ -94,7 +94,11 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
context.addOnDeviceProfileChangeListener(deviceProfileChangeListener)
gestureNavSettingsObserver.registerForCallingUser()
try {
gestureNavSettingsObserver.registerForCallingUser()
} catch (t: Throwable) {
// Ignore
}
}
fun onDestroy() {
@@ -103,8 +107,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
}
fun onTaskbarOrBubblebarWindowHeightOrInsetsChanged() {
val taskbarStashController = controllers.taskbarStashController
val tappableHeight = taskbarStashController.tappableHeightToReportToApps
val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps
// We only report tappableElement height for unstashed, persistent taskbar,
// which is also when we draw the rounded corners above taskbar.
val insetsRoundedCornerFlag =
@@ -118,7 +121,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
if (enableTaskbarNoRecreate() && controllers.sharedState != null) {
getProvidedInsets(
controllers.sharedState!!.insetsFrameProviders,
insetsRoundedCornerFlag,
insetsRoundedCornerFlag
)
} else {
getProvidedInsets(insetsRoundedCornerFlag)
@@ -130,32 +133,47 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
}
}
val bubbleControllers = controllers.bubbleControllers.getOrNull()
val taskbarTouchableHeight = taskbarStashController.touchableHeight
val taskbarTouchableHeight = controllers.taskbarStashController.touchableHeight
val bubblesTouchableHeight =
bubbleControllers?.bubbleStashController?.getTouchableHeight() ?: 0
// reset touch bounds
defaultTouchableRegion.setEmpty()
if (bubbleControllers != null) {
val bubbleBarViewController = bubbleControllers.bubbleBarViewController
val isBubbleBarVisible = bubbleControllers.bubbleStashController.isBubbleBarVisible()
val isAnimatingNewBubble = bubbleBarViewController.isAnimatingNewBubble
// if bubble bar is visible or animating new bubble, add bar bounds to the touch region
if (isBubbleBarVisible || isAnimatingNewBubble) {
defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.bubbleBarBounds)
defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.flyoutBounds)
if (controllers.bubbleControllers.isPresent) {
controllers.bubbleControllers.get().bubbleStashController.touchableHeight
} else {
0
}
}
val touchableHeight = max(taskbarTouchableHeight, bubblesTouchableHeight)
if (
taskbarStashController.isInApp ||
controllers.uiController.isInOverviewUi ||
context.showLockedTaskbarOnHome()
controllers.bubbleControllers.isPresent &&
controllers.bubbleControllers.get().bubbleStashController.isBubblesShowingOnHome
) {
// only add the taskbar touch region if not on home
val bottom = windowLayoutParams.height
val top = bottom - taskbarTouchableHeight
val right = context.deviceProfile.widthPx
defaultTouchableRegion.addBoundsToRegion(Rect(/* left= */ 0, top, right, bottom))
val iconBounds =
controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds
defaultTouchableRegion.set(
iconBounds.left,
iconBounds.top,
iconBounds.right,
iconBounds.bottom
)
} else {
defaultTouchableRegion.set(
0,
windowLayoutParams.height - touchableHeight,
context.deviceProfile.widthPx,
windowLayoutParams.height
)
// if there's an animating bubble add it to the touch region so that it's clickable
val isAnimatingNewBubble =
controllers.bubbleControllers
.getOrNull()
?.bubbleBarViewController
?.isAnimatingNewBubble
?: false
if (isAnimatingNewBubble) {
val iconBounds =
controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds
defaultTouchableRegion.op(iconBounds, Region.Op.UNION)
}
}
// Pre-calculate insets for different providers across different rotations for this gravity
@@ -180,7 +198,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
*/
private fun getProvidedInsets(
providedInsets: Array<InsetsFrameProvider>,
insetsRoundedCornerFlag: Int,
insetsRoundedCornerFlag: Int
): Array<InsetsFrameProvider> {
val navBarsFlag =
(if (context.isGestureNav) FLAG_SUPPRESS_SCRIM else 0) or insetsRoundedCornerFlag
@@ -206,14 +224,14 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
InsetsFrameProvider(insetsOwner, 0, navigationBars())
.setFlags(
navBarsFlag,
FLAG_SUPPRESS_SCRIM or FLAG_ANIMATE_RESIZING or FLAG_INSETS_ROUNDED_CORNER,
FLAG_SUPPRESS_SCRIM or FLAG_ANIMATE_RESIZING or FLAG_INSETS_ROUNDED_CORNER
),
InsetsFrameProvider(insetsOwner, 0, tappableElement()),
InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()),
InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures())
.setSource(SOURCE_DISPLAY),
InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures())
.setSource(SOURCE_DISPLAY),
.setSource(SOURCE_DISPLAY)
)
}
@@ -225,19 +243,20 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
provider.insetsSize = getInsetsForGravityWithCutout(contentHeight, gravity, endRotation)
} else if (provider.type == mandatorySystemGestures()) {
if (context.isThreeButtonNav) {
provider.insetsSize =
getInsetsForGravityWithCutout(contentHeight, gravity, endRotation)
provider.insetsSize = getInsetsForGravityWithCutout(contentHeight, gravity,
endRotation)
} else {
val gestureHeight =
ResourceUtils.getNavbarSize(
ResourceUtils.getNavbarSize(
ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
context.resources,
)
val isPinnedTaskbar =
context.deviceProfile.isTaskbarPresent && !context.isTransientTaskbar
val mandatoryGestureHeight = if (isPinnedTaskbar) contentHeight else gestureHeight
provider.insetsSize =
getInsetsForGravityWithCutout(mandatoryGestureHeight, gravity, endRotation)
context.resources)
val isPinnedTaskbar = context.deviceProfile.isTaskbarPresent
&& !context.deviceProfile.isTransientTaskbar
val mandatoryGestureHeight =
if (isPinnedTaskbar) contentHeight
else gestureHeight
provider.insetsSize = getInsetsForGravityWithCutout(mandatoryGestureHeight, gravity,
endRotation)
}
} else if (provider.type == tappableElement()) {
provider.insetsSize = getInsetsForGravity(tappableHeight, gravity)
@@ -256,7 +275,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
// When in gesture nav, report the stashed height to the IME, to allow hiding the
// IME navigation bar.
val imeInsetsSize =
if (context.isGestureNav) {
if (ENABLE_HIDE_IME_CAPTION_BAR && context.isGestureNav) {
getInsetsForGravity(controllers.taskbarStashController.stashedHeight, gravity)
} else {
getInsetsForGravity(taskbarHeightForIme, gravity)
@@ -270,8 +289,8 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
// override below (insetsSizeOverrides must have the same length and
// types after the window is added according to
// WindowManagerService#relayoutWindow)
provider.insetsSize,
),
provider.insetsSize
)
)
// Use 0 tappableElement insets for the VoiceInteractionWindow when gesture nav is enabled.
val visInsetsSizeForTappableElement =
@@ -282,7 +301,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize),
InsetsFrameProvider.InsetsSizeOverride(
TYPE_VOICE_INTERACTION,
visInsetsSizeForTappableElement,
visInsetsSizeForTappableElement
),
)
if (
@@ -344,21 +363,24 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
*/
fun updateInsetsTouchability(insetsInfo: ViewTreeObserver.InternalInsetsInfo) {
insetsInfo.touchableRegion.setEmpty()
// Always have nav buttons be touchable
controllers.navbarButtonsViewController.addVisibleButtonsRegion(
context.dragLayer,
insetsInfo.touchableRegion
)
debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds)
val bubbleBarVisible =
controllers.bubbleControllers.isPresent &&
controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible()
var insetsIsTouchableRegion = true
// Prevents the taskbar from taking touches and conflicting with setup wizard
if (
context.isPhoneButtonNavMode &&
context.isUserSetupComplete &&
(!controllers.navbarButtonsViewController.isImeVisible ||
!controllers.navbarButtonsViewController.isImeRenderingNavButtons)
) {
insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME)
insetsIsTouchableRegion = false
debugTouchableRegion.lastSetTouchableReason =
"Phone button nav mode: Fullscreen touchable, IME not affecting nav buttons"
} else if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
// Let touches pass through us.
insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
@@ -390,7 +412,10 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
bubbleBarVisible
) {
// Taskbar has some touchable elements, take over the full taskbar area
if (controllers.uiController.isInOverviewUi && context.isTransientTaskbar) {
if (
controllers.uiController.isInOverviewUi &&
DisplayController.isTransientTaskbar(context)
) {
val region =
controllers.taskbarActivityContext.dragLayer.lastDrawnTransientRect.toRegion()
val bubbleBarBounds =
@@ -406,7 +431,7 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
// Include the bounds of the bubble bar in the touchable region if they exist.
if (bubbleBarBounds != null) {
region.addBoundsToRegion(bubbleBarBounds)
region.op(bubbleBarBounds, Region.Op.UNION)
}
insetsInfo.touchableRegion.set(region)
debugTouchableRegion.lastSetTouchableReason = "Transient Taskbar is in Overview"
@@ -423,12 +448,6 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
debugTouchableRegion.lastSetTouchableReason =
"Icons are not visible, but other components such as 3 buttons might be"
}
// Always have nav buttons be touchable
controllers.navbarButtonsViewController.addVisibleButtonsRegion(
context.dragLayer,
insetsInfo.touchableRegion,
)
debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds)
context.excludeFromMagnificationRegion(insetsIsTouchableRegion)
}
@@ -16,26 +16,16 @@
package com.android.launcher3.taskbar;
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.app.animation.Interpolators.FINAL_FRAME;
import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_ALIGNMENT;
import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_STASH;
import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
import static com.android.launcher3.Utilities.isRtl;
import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP;
import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW;
import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE;
import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_FOR_BUBBLES;
import static com.android.launcher3.taskbar.TaskbarStashController.UNLOCK_TRANSITION_MEMOIZATION_MS;
import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_HOME;
import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_IN_ANIM_ALPHA_DURATION_MS;
import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_OUT_ANIM_POSITION_DURATION_MS;
import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
import static com.android.quickstep.fallback.RecentsStateUtilsKt.toLauncherState;
import static com.android.quickstep.util.SystemUiFlagUtils.isTaskbarHidden;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK;
import static com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_AWAKE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -43,43 +33,32 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.os.SystemClock;
import android.util.Log;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.app.animation.Interpolators;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Hotseat;
import com.android.launcher3.Hotseat.HotseatQsbAlphaId;
import com.android.launcher3.LauncherState;
import com.android.launcher3.QuickstepTransitionManager;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.quickstep.RecentsAnimationCallbacks;
import com.android.quickstep.RecentsAnimationController;
import com.android.quickstep.fallback.RecentsState;
import com.android.quickstep.fallback.window.RecentsDisplayModel;
import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.fallback.window.RecentsWindowManager;
import com.android.quickstep.util.ScalingWorkspaceRevealAnim;
import com.android.quickstep.util.SystemUiFlagUtils;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.animation.ViewRootSync;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.StringJoiner;
import java.util.function.Consumer;
/**
* Track LauncherState, RecentsAnimation, resumed state for task bar in one place here and animate
@@ -164,23 +143,16 @@ public class TaskbarLauncherStateController {
private AnimatedFloat mTaskbarBackgroundAlpha;
private AnimatedFloat mTaskbarAlpha;
private AnimatedFloat mTaskbarCornerRoundness;
private MultiProperty mTaskbarAlphaForHome;
private @Nullable Animator mHotseatTranslationXAnimation;
private MultiProperty mIconAlphaForHome;
private QuickstepLauncher mLauncher;
private boolean mIsDestroyed = false;
private Integer mPrevState;
private int mState;
private LauncherState mLauncherState = LauncherState.NORMAL;
private boolean mSkipNextRecentsAnimEnd;
// Time when FLAG_TASKBAR_HIDDEN was last cleared, SystemClock.elapsedRealtime (milliseconds).
private long mLastRemoveTaskbarHiddenTimeMs = 0;
/**
* Time when FLAG_DEVICE_LOCKED was last cleared, plus
* {@link TaskbarStashController#UNLOCK_TRANSITION_MEMOIZATION_MS}
*/
private long mLastUnlockTransitionTimeout;
private long mLastUnlockTimeMs = 0;
private @Nullable TaskBarRecentsAnimationListener mTaskBarRecentsAnimationListener;
@@ -188,15 +160,11 @@ public class TaskbarLauncherStateController {
private boolean mShouldDelayLauncherStateAnim;
private @Nullable BubbleBarLocation mBubbleBarLocation;
// We skip any view synchronizations during init/destroy.
private boolean mCanSyncViews;
private boolean mIsQsbInline;
private RecentsAnimationCallbacks mRecentsAnimationCallbacks;
private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener =
new DeviceProfile.OnDeviceProfileChangeListener() {
@Override
@@ -204,18 +172,16 @@ public class TaskbarLauncherStateController {
if (mIsQsbInline && !dp.isQsbInline) {
// We only modify QSB alpha if isQsbInline = true. If we switch to a DP
// where isQsbInline = false, then we need to reset the alpha.
mLauncher.getHotseat().setQsbAlpha(1f, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
mLauncher.getHotseat().setQsbAlpha(1f);
}
mIsQsbInline = dp.isQsbInline;
TaskbarLauncherStateController.this.updateIconAlphaForHome(
mTaskbarAlphaForHome.getValue(), ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
TaskbarLauncherStateController.this.onBubbleBarLocationChanged(
mBubbleBarLocation, /* animate = */ false);
mIconAlphaForHome.getValue());
}
};
private final StateManager.StateListener<LauncherState> mStateListener =
new StateManager.StateListener<>() {
new StateManager.StateListener<LauncherState>() {
@Override
public void onStateTransitionStart(LauncherState toState) {
@@ -229,11 +195,7 @@ public class TaskbarLauncherStateController {
updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, true);
if (!mShouldDelayLauncherStateAnim) {
if (toState == LauncherState.NORMAL) {
TaskbarActivityContext activity = mControllers.taskbarActivityContext;
boolean isPinnedTaskbarAndNotInDesktopMode =
!activity.isInDesktopMode() && activity.isPinnedTaskbar();
applyState(QuickstepTransitionManager.getTaskbarToHomeDuration(
isPinnedTaskbarAndNotInDesktopMode));
applyState(QuickstepTransitionManager.getTaskbarToHomeDuration());
} else {
applyState();
}
@@ -249,20 +211,6 @@ public class TaskbarLauncherStateController {
}
};
private final StateManager.StateListener<RecentsState> mRecentsStateListener =
new StateManager.StateListener<>() {
@Override
public void onStateTransitionStart(RecentsState toState) {
mStateListener.onStateTransitionStart(toLauncherState(toState));
}
@Override
public void onStateTransitionComplete(RecentsState finalState) {
mStateListener.onStateTransitionComplete(toLauncherState(finalState));
}
};
/**
* Callback for when launcher state transition completes after user swipes to home.
* @param finalState The final state of the transition.
@@ -291,43 +239,31 @@ public class TaskbarLauncherStateController {
.getTaskbarBackgroundAlpha();
mTaskbarAlpha = mControllers.taskbarDragLayerController.getTaskbarAlpha();
mTaskbarCornerRoundness = mControllers.getTaskbarCornerRoundness();
mTaskbarAlphaForHome = mControllers.taskbarViewController
mIconAlphaForHome = mControllers.taskbarViewController
.getTaskbarIconAlpha().get(ALPHA_INDEX_HOME);
resetIconAlignment();
if (!mControllers.taskbarActivityContext.isPhoneMode()) {
mLauncher.getStateManager().addStateListener(mStateListener);
runForRecentsWindowManager(recentsWindowManager ->
recentsWindowManager.getStateManager().addStateListener(mRecentsStateListener));
}
mLauncher.getStateManager().addStateListener(mStateListener);
mLauncherState = launcher.getStateManager().getState();
updateStateForSysuiFlags(sysuiStateFlags, /*applyState*/ false);
applyState(0);
mCanSyncViews = !mControllers.taskbarActivityContext.isPhoneMode();
mCanSyncViews = true;
mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
updateOverviewDragState(mLauncherState);
}
public void onDestroy() {
mIsDestroyed = true;
mCanSyncViews = false;
if (mRecentsAnimationCallbacks != null) {
mRecentsAnimationCallbacks.removeListener(mTaskBarRecentsAnimationListener);
mRecentsAnimationCallbacks = null;
}
mIconAlignment.finishAnimation();
mLauncher.getHotseat().setIconsAlpha(1f, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
mLauncher.getHotseat().setIconsAlpha(1f);
mLauncher.getStateManager().removeStateListener(mStateListener);
runForRecentsWindowManager(recentsWindowManager ->
recentsWindowManager.getStateManager().removeStateListener(mRecentsStateListener));
mCanSyncViews = !mControllers.taskbarActivityContext.isPhoneMode();
mCanSyncViews = true;
mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
}
@@ -342,7 +278,6 @@ public class TaskbarLauncherStateController {
// If going to overview, stash the task bar
// If going home, align the icons to hotseat
AnimatorSet animatorSet = new AnimatorSet();
mRecentsAnimationCallbacks = callbacks;
// Update stashed flags first to ensure goingToUnstashedLauncherState() returns correctly.
TaskbarStashController stashController = mControllers.taskbarStashController;
@@ -354,7 +289,6 @@ public class TaskbarLauncherStateController {
stashController.updateStateForFlag(FLAG_IN_APP, false);
updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, true);
mLauncherState = toState;
animatorSet.play(stashController.createApplyStateAnimator(duration));
animatorSet.play(applyState(duration, false));
@@ -364,15 +298,12 @@ public class TaskbarLauncherStateController {
}
mTaskBarRecentsAnimationListener = new TaskBarRecentsAnimationListener(callbacks);
callbacks.addListener(mTaskBarRecentsAnimationListener);
RecentsView recentsView = mControllers.uiController.getRecentsView();
if (recentsView != null) {
recentsView.setTaskLaunchListener(() -> mTaskBarRecentsAnimationListener
.endGestureStateOverride(true, false /*canceled*/));
recentsView.setTaskLaunchCancelledRunnable(() -> {
updateStateForUserFinishedToApp(false /* finishedToApp */);
});
}
((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(() ->
mTaskBarRecentsAnimationListener.endGestureStateOverride(true, false /*canceled*/));
((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchCancelledRunnable(() -> {
updateStateForUserFinishedToApp(false /* finishedToApp */);
});
return animatorSet;
}
@@ -414,7 +345,14 @@ public class TaskbarLauncherStateController {
updateStateForFlag(FLAG_DEVICE_LOCKED, SystemUiFlagUtils.isLocked(systemUiStateFlags));
updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags));
// Taskbar is hidden whenever the device is dreaming. The dreaming state includes the
// interactive dreams, AoD, screen off. Since the SYSUI_STATE_DEVICE_DREAMING only kicks in
// when the device is asleep, the second condition extends ensures that the transition from
// and to the WAKEFULNESS_ASLEEP state also hide the taskbar, and improves the taskbar
// hide/reveal animation timings.
boolean isTaskbarHidden = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_DEVICE_DREAMING)
|| (systemUiStateFlags & SYSUI_STATE_WAKEFULNESS_MASK) != WAKEFULNESS_AWAKE;
updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden);
if (applyState) {
applyState();
@@ -427,7 +365,10 @@ public class TaskbarLauncherStateController {
* @param launcherState The current state launcher is in
*/
private void updateOverviewDragState(LauncherState launcherState) {
boolean disallowLongClick = mLauncher.isSplitSelectionActive() || mIsAnimatingToLauncher;
boolean disallowLongClick =
FeatureFlags.enableSplitContextually()
? mLauncher.isSplitSelectionActive()
: launcherState == LauncherState.OVERVIEW_SPLIT_SELECT;
com.android.launcher3.taskbar.Utilities.setOverviewDragState(
mControllers, launcherState.disallowTaskbarGlobalDrag(),
disallowLongClick, launcherState.allowTaskbarInitialSplitSelection());
@@ -466,7 +407,7 @@ public class TaskbarLauncherStateController {
}
public Animator applyState(long duration, boolean start) {
if (mIsDestroyed || mControllers.taskbarActivityContext.isPhoneMode()) {
if (mControllers.taskbarActivityContext.isDestroyed()) {
return null;
}
Animator animator = null;
@@ -494,32 +435,23 @@ public class TaskbarLauncherStateController {
private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) {
final boolean isInLauncher = isInLauncher();
final boolean isInOverview = mControllers.uiController.isInOverviewUi();
final boolean isIconAlignedWithHotseat = isIconAlignedWithHotseat();
final float toAlignment = isIconAlignedWithHotseat ? 1 : 0;
boolean handleOpenFloatingViews = false;
boolean isPinnedTaskbar =
mControllers.taskbarActivityContext.isPinnedTaskbar();
if (DEBUG) {
Log.d(TAG, "onStateChangeApplied - isInLauncher: " + isInLauncher
+ ", mLauncherState: " + mLauncherState
+ ", toAlignment: " + toAlignment);
}
mControllers.bubbleControllers.ifPresent(controllers -> {
// Show the bubble bar when on launcher home (hotseat icons visible) or in overview
boolean onOverview = isInLauncher && mLauncherState == LauncherState.OVERVIEW;
boolean hotseatIconsVisible = isInLauncher && mLauncherState.areElementsVisible(
mLauncher, HOTSEAT_ICONS);
BubbleLauncherState state = onOverview
? BubbleLauncherState.OVERVIEW
: hotseatIconsVisible
? BubbleLauncherState.HOME
: BubbleLauncherState.IN_APP;
controllers.bubbleStashController.setLauncherState(state);
// Show the bubble bar when on launcher home or in overview.
boolean onHome = isInLauncher && mLauncherState == LauncherState.NORMAL;
boolean onOverview = mLauncherState == LauncherState.OVERVIEW;
controllers.bubbleStashController.setBubblesShowingOnHome(onHome);
controllers.bubbleStashController.setBubblesShowingOnOverview(onOverview);
});
TaskbarStashController stashController = mControllers.taskbarStashController;
stashController.updateStateForFlag(FLAG_IN_OVERVIEW,
mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_OVERVIEW,
mLauncherState == LauncherState.OVERVIEW);
AnimatorSet animatorSet = new AnimatorSet();
@@ -538,8 +470,7 @@ public class TaskbarLauncherStateController {
// We're changing state to home, should close open popups e.g. Taskbar AllApps
handleOpenFloatingViews = true;
}
if (mLauncherState == LauncherState.OVERVIEW
&& !mControllers.taskbarActivityContext.isPhoneMode()) {
if (mLauncherState == LauncherState.OVERVIEW) {
// Calling to update the insets in TaskbarInsetController#updateInsetsTouchability
mControllers.taskbarActivityContext.notifyUpdateLayoutParams();
}
@@ -551,6 +482,8 @@ public class TaskbarLauncherStateController {
public void onAnimationStart(Animator animation) {
mIsAnimatingToLauncher = isInLauncher;
TaskbarStashController stashController =
mControllers.taskbarStashController;
if (DEBUG) {
Log.d(TAG, "onAnimationStart - FLAG_IN_APP: " + !isInLauncher);
}
@@ -566,8 +499,6 @@ public class TaskbarLauncherStateController {
// Handle closing open popups when going home/overview
handleOpenFloatingViews = true;
} else {
stashController.applyState();
}
if (handleOpenFloatingViews && isInLauncher) {
@@ -576,7 +507,7 @@ public class TaskbarLauncherStateController {
if (hasAnyFlag(changedFlags, FLAG_TASKBAR_HIDDEN) && !hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
// Take note of the current time, as the taskbar is made visible again.
mLastRemoveTaskbarHiddenTimeMs = SystemClock.elapsedRealtime();
mLastUnlockTimeMs = SystemClock.elapsedRealtime();
}
boolean isHidden = hasAnyFlag(FLAG_TASKBAR_HIDDEN);
@@ -602,8 +533,7 @@ public class TaskbarLauncherStateController {
// with a fingerprint reader. This should only be done when the device was woken
// up via fingerprint reader, however since this information is currently not
// available, opting to always delay the fade-in a bit.
long durationSinceLastUnlockMs = SystemClock.elapsedRealtime()
- mLastRemoveTaskbarHiddenTimeMs;
long durationSinceLastUnlockMs = SystemClock.elapsedRealtime() - mLastUnlockTimeMs;
taskbarVisibility.setStartDelay(
Math.max(0, TASKBAR_SHOW_DELAY_MS - durationSinceLastUnlockMs));
}
@@ -611,18 +541,10 @@ public class TaskbarLauncherStateController {
}
float backgroundAlpha = isInLauncher && isTaskbarAlignedWithHotseat() ? 0 : 1;
AnimatedFloat taskbarBgOffset =
mControllers.taskbarDragLayerController.getTaskbarBackgroundOffset();
boolean showTaskbar = shouldShowTaskbar(mControllers.taskbarActivityContext, isInLauncher,
isInOverview);
float taskbarBgOffsetEnd = showTaskbar ? 0f : 1f;
float taskbarBgOffsetStart = showTaskbar ? 1f : 0f;
// Don't animate if background has reached desired value.
if (mTaskbarBackgroundAlpha.isAnimating()
|| mTaskbarBackgroundAlpha.value != backgroundAlpha
|| taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart)
|| taskbarBgOffset.value != taskbarBgOffsetEnd) {
|| mTaskbarBackgroundAlpha.value != backgroundAlpha) {
mTaskbarBackgroundAlpha.cancelAnimation();
if (DEBUG) {
Log.d(TAG, "onStateChangeApplied - taskbarBackgroundAlpha - "
@@ -633,48 +555,30 @@ public class TaskbarLauncherStateController {
boolean isInLauncherIconNotAligned = isInLauncher && !isIconAlignedWithHotseat;
boolean notInLauncherIconNotAligned = !isInLauncher && !isIconAlignedWithHotseat;
boolean isInLauncherIconIsAligned = isInLauncher && isIconAlignedWithHotseat;
// When Hotseat icons are not on top don't change duration or add start delay.
// This will keep the duration in sync for icon alignment and background fade in/out.
// For example, launching app from launcher all apps.
boolean isHotseatIconOnTopWhenAligned =
mControllers.uiController.isHotseatIconOnTopWhenAligned();
float startDelay = 0;
// We want to delay the background from fading in so that the icons have time to move
// into the bounds of the background before it appears.
if (isInLauncherIconNotAligned) {
startDelay = duration * TASKBAR_BG_ALPHA_LAUNCHER_NOT_ALIGNED_DELAY_MULT;
} else if (notInLauncherIconNotAligned && isHotseatIconOnTopWhenAligned) {
} else if (notInLauncherIconNotAligned) {
startDelay = duration * TASKBAR_BG_ALPHA_NOT_LAUNCHER_NOT_ALIGNED_DELAY_MULT;
}
float newDuration = duration - startDelay;
if (isInLauncherIconIsAligned && isHotseatIconOnTopWhenAligned) {
if (isInLauncherIconIsAligned) {
// Make the background fade out faster so that it is gone by the time the
// icons move outside of the bounds of the background.
newDuration = duration * TASKBAR_BG_ALPHA_LAUNCHER_IS_ALIGNED_DURATION_MULT;
}
Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha.animateToValue(
backgroundAlpha);
if (isPinnedTaskbar) {
setupPinnedTaskbarAnimation(animatorSet, showTaskbar, taskbarBgOffset,
taskbarBgOffsetStart, taskbarBgOffsetEnd, duration, taskbarBackgroundAlpha);
} else {
taskbarBackgroundAlpha.setDuration((long) newDuration);
taskbarBackgroundAlpha.setStartDelay((long) startDelay);
}
Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha
.animateToValue(backgroundAlpha)
.setDuration((long) newDuration);
taskbarBackgroundAlpha.setStartDelay((long) startDelay);
animatorSet.play(taskbarBackgroundAlpha);
}
float cornerRoundness = isInLauncher ? 0 : 1;
if (mControllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview(
mControllers.taskbarActivityContext.getDisplayId())
&& mControllers.getSharedState() != null) {
cornerRoundness =
mControllers.taskbarDesktopModeController.getTaskbarCornerRoundness(
mControllers.getSharedState().showCornerRadiusInDesktopMode);
}
// Don't animate if corner roundness has reached desired value.
if (mTaskbarCornerRoundness.isAnimating()
|| mTaskbarCornerRoundness.value != cornerRoundness) {
@@ -692,15 +596,6 @@ public class TaskbarLauncherStateController {
boolean isUnlockTransition =
hasAnyFlag(changedFlags, FLAG_DEVICE_LOCKED) && !hasAnyFlag(FLAG_DEVICE_LOCKED);
if (isUnlockTransition) {
// the launcher might not be resumed at the time the device is considered
// unlocked (when the keyguard goes away), but possibly shortly afterwards.
// To play the unlock transition at the time the unstash animation actually happens,
// this memoizes the state transition for UNLOCK_TRANSITION_MEMOIZATION_MS.
mLastUnlockTransitionTimeout =
SystemClock.elapsedRealtime() + UNLOCK_TRANSITION_MEMOIZATION_MS;
}
boolean isInUnlockTimeout = SystemClock.elapsedRealtime() < mLastUnlockTransitionTimeout;
if (isUnlockTransition || isInUnlockTimeout) {
// When transitioning to unlocked, ensure the hotseat is fully visible from the
// beginning. The hotseat itself is animated by LauncherUnlockAnimationController.
mIconAlignment.cancelAnimation();
@@ -716,11 +611,8 @@ public class TaskbarLauncherStateController {
} else if (mIconAlignment.isAnimatingToValue(toAlignment)
|| mIconAlignment.isSettledOnValue(toAlignment)) {
// Already at desired value, but make sure we run the callback at the end.
animatorSet.addListener(AnimatorListeners.forEndCallback(() -> {
if (!mIconAlignment.isAnimating()) {
onIconAlignmentRatioChanged();
}
}));
animatorSet.addListener(AnimatorListeners.forEndCallback(
this::onIconAlignmentRatioChanged));
} else {
mIconAlignment.cancelAnimation();
ObjectAnimator iconAlignAnim = mIconAlignment
@@ -731,19 +623,10 @@ public class TaskbarLauncherStateController {
+ mIconAlignment.value
+ " -> " + toAlignment + ": " + duration);
}
if (!isPinnedTaskbar) {
if (hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
iconAlignAnim.setInterpolator(FINAL_FRAME);
} else {
animatorSet.play(iconAlignAnim);
}
}
animatorSet.play(iconAlignAnim);
}
Interpolator interpolator = enableScalingRevealHomeAnimation() && !isPinnedTaskbar
? ScalingWorkspaceRevealAnim.SCALE_INTERPOLATOR : EMPHASIZED;
animatorSet.setInterpolator(interpolator);
animatorSet.setInterpolator(EMPHASIZED);
if (start) {
animatorSet.start();
@@ -751,75 +634,12 @@ public class TaskbarLauncherStateController {
return animatorSet;
}
private static boolean shouldShowTaskbar(TaskbarActivityContext activityContext,
boolean isInLauncher, boolean isInOverview) {
if (activityContext.showDesktopTaskbarForFreeformDisplay()) {
return true;
}
if (activityContext.showLockedTaskbarOnHome() && isInLauncher) {
return true;
}
return !isInLauncher || isInOverview;
}
private void setupPinnedTaskbarAnimation(AnimatorSet animatorSet, boolean showTaskbar,
AnimatedFloat taskbarBgOffset, float taskbarBgOffsetStart, float taskbarBgOffsetEnd,
long duration, Animator taskbarBackgroundAlpha) {
float targetAlpha = !showTaskbar ? 1 : 0;
mLauncher.getHotseat().setIconsAlpha(targetAlpha, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
if (mIsQsbInline) {
mLauncher.getHotseat().setQsbAlpha(targetAlpha,
ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
}
if ((taskbarBgOffset.value != taskbarBgOffsetEnd && !taskbarBgOffset.isAnimating())
|| taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart)) {
taskbarBgOffset.cancelAnimation();
Animator taskbarIconAlpha = mTaskbarAlphaForHome.animateToValue(
showTaskbar ? 1f : 0f);
AnimatedFloat taskbarIconTranslationYForHome =
mControllers.taskbarViewController.mTaskbarIconTranslationYForHome;
ObjectAnimator taskbarBackgroundOffset = taskbarBgOffset.animateToValue(
taskbarBgOffsetStart,
taskbarBgOffsetEnd);
ObjectAnimator taskbarIconsYTranslation = null;
float taskbarHeight =
mControllers.taskbarActivityContext.getDeviceProfile().taskbarHeight;
if (showTaskbar) {
taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue(
taskbarHeight, 0);
} else {
taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue(0,
taskbarHeight);
}
taskbarIconAlpha.setDuration(duration);
taskbarIconsYTranslation.setDuration(duration);
taskbarBackgroundOffset.setDuration(duration);
animatorSet.play(taskbarIconAlpha);
animatorSet.play(taskbarIconsYTranslation);
animatorSet.play(taskbarBackgroundOffset);
}
taskbarBackgroundAlpha.setInterpolator(showTaskbar ? INSTANT : FINAL_FRAME);
taskbarBackgroundAlpha.setDuration(duration);
}
/**
* Whether the taskbar is aligned with the hotseat in the current/target launcher state.
*
* This refers to the intended state - a transition to this state might be in progress.
*/
public boolean isTaskbarAlignedWithHotseat() {
if (mControllers.taskbarActivityContext.showDesktopTaskbarForFreeformDisplay()) {
return false;
}
if (mControllers.taskbarActivityContext.showLockedTaskbarOnHome() && isInLauncher()) {
return false;
}
return mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
}
@@ -831,7 +651,8 @@ public class TaskbarLauncherStateController {
boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
boolean willStashVisually = isInStashedState
&& mControllers.taskbarStashController.supportsVisualStashing();
boolean isTaskbarAlignedWithHotseat = isTaskbarAlignedWithHotseat();
boolean isTaskbarAlignedWithHotseat =
mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
return isTaskbarAlignedWithHotseat && !willStashVisually;
} else {
return false;
@@ -850,15 +671,6 @@ public class TaskbarLauncherStateController {
return mLauncherState.isRecentsViewVisible;
}
/**
* Returns the current mLauncherState. Note that this could represent RecentsState as well, as
* we convert those to equivalent LauncherStates even if Launcher Activity is not actually in
* those states (for the case where the state is represented in a separate Window instead).
*/
public LauncherState getLauncherState() {
return mLauncherState;
}
private void playStateTransitionAnim(AnimatorSet animatorSet, long duration,
boolean committed) {
boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher);
@@ -871,17 +683,14 @@ public class TaskbarLauncherStateController {
public void onAnimationEnd(Animator animation) {
if (isInStashedState && committed) {
// Reset hotseat alpha to default
mLauncher.getHotseat().setIconsAlpha(1, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
mLauncher.getHotseat().setIconsAlpha(1);
}
}
@Override
public void onAnimationStart(Animator animation) {
float hotseatIconsAlpha = mLauncher.getHotseat()
.getIconsAlpha(ALPHA_CHANNEL_TASKBAR_ALIGNMENT)
.getValue();
if (hotseatIconsAlpha > 0) {
updateIconAlphaForHome(hotseatIconsAlpha, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
if (mLauncher.getHotseat().getIconsAlpha() > 0) {
updateIconAlphaForHome(mLauncher.getHotseat().getIconsAlpha());
}
}
});
@@ -910,36 +719,6 @@ public class TaskbarLauncherStateController {
}
}
protected void stashHotseat(boolean stash) {
// align taskbar with the hotseat icons before performing any animation
mControllers.taskbarViewController.setLauncherIconAlignment(/* alignmentRatio = */ 1,
mLauncher.getDeviceProfile());
TaskbarStashController stashController = mControllers.taskbarStashController;
stashController.updateStateForFlag(FLAG_STASHED_FOR_BUBBLES, stash);
Runnable swapHotseatWithTaskbar = new Runnable() {
@Override
public void run() {
updateIconAlphaForHome(stash ? 1 : 0, ALPHA_CHANNEL_TASKBAR_STASH);
}
};
if (stash) {
stashController.applyState();
// if we stashing the hotseat we need to immediately swap it with the animating taskbar
swapHotseatWithTaskbar.run();
} else {
// if we revert stashing make swap after taskbar animation is complete
stashController.applyState(/* postApplyAction = */ swapHotseatWithTaskbar);
}
}
protected void unStashHotseatInstantly() {
TaskbarStashController stashController = mControllers.taskbarStashController;
stashController.updateStateForFlag(FLAG_STASHED_FOR_BUBBLES, false);
stashController.applyState(/* duration = */ 0);
updateIconAlphaForHome(/* taskbarAlpha = */ 0,
ALPHA_CHANNEL_TASKBAR_STASH, /* updateTaskbarAlpha = */ false);
}
/**
* Resets and updates the icon alignment.
*/
@@ -949,7 +728,7 @@ public class TaskbarLauncherStateController {
}
private void onIconAlignmentRatioChanged() {
float currentValue = mTaskbarAlphaForHome.getValue();
float currentValue = mIconAlphaForHome.getValue();
boolean taskbarWillBeVisible = mIconAlignment.value < 1;
boolean firstFrameVisChanged = (taskbarWillBeVisible && Float.compare(currentValue, 1) != 0)
|| (!taskbarWillBeVisible && Float.compare(currentValue, 0) != 0);
@@ -957,33 +736,24 @@ public class TaskbarLauncherStateController {
mControllers.taskbarViewController.setLauncherIconAlignment(
mIconAlignment.value, mLauncher.getDeviceProfile());
mControllers.navbarButtonsViewController.updateTaskbarAlignment(mIconAlignment.value);
// Switch taskbar and hotseat in last frame and if taskbar is not hidden for bubbles
boolean isHiddenForBubbles = mControllers.taskbarStashController.isHiddenForBubbles();
updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0, ALPHA_CHANNEL_TASKBAR_ALIGNMENT,
/* updateTaskbarAlpha = */ !isHiddenForBubbles);
// Switch taskbar and hotseat in last frame
updateIconAlphaForHome(taskbarWillBeVisible ? 1 : 0);
// Sync the first frame where we swap taskbar and hotseat.
if (firstFrameVisChanged && mCanSyncViews && !Utilities.isRunningInTestHarness()) {
ViewRootSync.synchronizeNextDraw(mLauncher.getHotseat(),
mControllers.taskbarActivityContext.getDragLayer(),
() -> {});
() -> {
});
}
}
private void updateIconAlphaForHome(float taskbarAlpha, @HotseatQsbAlphaId int alphaChannel) {
updateIconAlphaForHome(taskbarAlpha, alphaChannel, /* updateTaskbarAlpha = */ true);
}
private void updateIconAlphaForHome(float taskbarAlpha,
@HotseatQsbAlphaId int alphaChannel,
boolean updateTaskbarAlpha) {
if (mIsDestroyed) {
private void updateIconAlphaForHome(float alpha) {
if (mControllers.taskbarActivityContext.isDestroyed()) {
return;
}
if (updateTaskbarAlpha) {
mTaskbarAlphaForHome.setValue(taskbarAlpha);
}
boolean hotseatVisible = taskbarAlpha == 0
mIconAlphaForHome.setValue(alpha);
boolean hotseatVisible = alpha == 0
|| mControllers.taskbarActivityContext.isPhoneMode()
|| (!mControllers.uiController.isHotseatIconOnTopWhenAligned()
&& mIconAlignment.value > 0);
@@ -991,71 +761,12 @@ public class TaskbarLauncherStateController {
* Hide Launcher Hotseat icons when Taskbar icons have opacity. Both icon sets
* should not be visible at the same time.
*/
float targetAlpha = hotseatVisible ? 1 : 0;
mLauncher.getHotseat().setIconsAlpha(targetAlpha, alphaChannel);
mLauncher.getHotseat().setIconsAlpha(hotseatVisible ? 1 : 0);
if (mIsQsbInline) {
mLauncher.getHotseat().setQsbAlpha(targetAlpha, alphaChannel);
mLauncher.getHotseat().setQsbAlpha(hotseatVisible ? 1 : 0);
}
}
/** Updates launcher home screen appearance accordingly to the bubble bar location. */
public void onBubbleBarLocationChanged(@Nullable BubbleBarLocation location, boolean animate) {
mBubbleBarLocation = location;
if (location == null) {
// bubble bar is not present, hence no location, resetting the hotseat
updateHotseatAndQsbTranslationX(/* targetValue = */ 0, animate);
mBubbleBarLocation = null;
return;
}
DeviceProfile deviceProfile = mLauncher.getDeviceProfile();
if (!deviceProfile.shouldAdjustHotseatOnNavBarLocationUpdate(
mControllers.taskbarActivityContext)) {
return;
}
boolean isBubblesOnLeft = location.isOnLeft(isRtl(mLauncher.getResources()));
int targetX = deviceProfile
.getHotseatTranslationXForNavBar(mLauncher, isBubblesOnLeft);
updateHotseatAndQsbTranslationX(targetX, animate);
}
/** Used to translate hotseat and QSB to make room for bubbles. */
private void updateHotseatAndQsbTranslationX(float targetValue, boolean animate) {
// cancel existing animation
if (mHotseatTranslationXAnimation != null) {
mHotseatTranslationXAnimation.cancel();
mHotseatTranslationXAnimation = null;
}
Hotseat hotseat = mLauncher.getHotseat();
AnimatorSet translationXAnimation = new AnimatorSet();
MultiProperty iconsTranslationX = mLauncher.getHotseat()
.getIconsTranslationX(Hotseat.ICONS_TRANSLATION_X_NAV_BAR_ALIGNMENT);
if (animate) {
translationXAnimation.playTogether(iconsTranslationX.animateToValue(targetValue));
} else {
iconsTranslationX.setValue(targetValue);
}
float qsbTargetX = 0;
if (mIsQsbInline) {
qsbTargetX = targetValue;
}
MultiProperty qsbTranslationX = hotseat.getQsbTranslationX();
if (qsbTranslationX != null) {
if (animate) {
translationXAnimation.playTogether(qsbTranslationX.animateToValue(qsbTargetX));
} else {
qsbTranslationX.setValue(qsbTargetX);
}
}
if (!animate) {
return;
}
mHotseatTranslationXAnimation = translationXAnimation;
translationXAnimation.setStartDelay(FADE_OUT_ANIM_POSITION_DURATION_MS);
translationXAnimation.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS);
translationXAnimation.setInterpolator(Interpolators.EMPHASIZED);
translationXAnimation.start();
}
private final class TaskBarRecentsAnimationListener implements
RecentsAnimationCallbacks.RecentsAnimationListener {
private final RecentsAnimationCallbacks mCallbacks;
@@ -1072,12 +783,7 @@ public class TaskbarLauncherStateController {
@Override
public void onRecentsAnimationFinished(RecentsAnimationController controller) {
endGestureStateOverride(!controller.getFinishTargetIsLauncher(),
controller.getLauncherIsVisibleAtFinish(), false /*canceled*/);
}
private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
endGestureStateOverride(finishedToApp, finishedToApp, canceled);
endGestureStateOverride(!controller.getFinishTargetIsLauncher(), false /*canceled*/);
}
/**
@@ -1087,45 +793,29 @@ public class TaskbarLauncherStateController {
*
* @param finishedToApp {@code true} if the recents animation finished to showing an app and
* not workspace or overview
* @param launcherIsVisible {code true} if launcher is visible at finish
* @param canceled {@code true} if the recents animation was canceled instead of
* finishing
* to completion
* @param canceled {@code true} if the recents animation was canceled instead of finishing
* to completion
*/
private void endGestureStateOverride(boolean finishedToApp, boolean launcherIsVisible,
boolean canceled) {
private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
mCallbacks.removeListener(this);
mTaskBarRecentsAnimationListener = null;
RecentsView recentsView = mControllers.uiController.getRecentsView();
if (recentsView != null) {
recentsView.setTaskLaunchListener(null);
}
((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(null);
if (mSkipNextRecentsAnimEnd && !canceled) {
mSkipNextRecentsAnimEnd = false;
return;
}
updateStateForUserFinishedToApp(finishedToApp, launcherIsVisible);
updateStateForUserFinishedToApp(finishedToApp);
}
}
/**
* @see #updateStateForUserFinishedToApp(boolean, boolean)
* Updates the visible state immediately to ensure a seamless handoff.
* @param finishedToApp True iff user is in an app.
*/
private void updateStateForUserFinishedToApp(boolean finishedToApp) {
updateStateForUserFinishedToApp(finishedToApp, !finishedToApp);
}
/**
* Updates the visible state immediately to ensure a seamless handoff.
*
* @param finishedToApp True iff user is in an app.
* @param launcherIsVisible True iff launcher is still visible (ie. transparent app)
*/
private void updateStateForUserFinishedToApp(boolean finishedToApp,
boolean launcherIsVisible) {
// Update the visible state immediately to ensure a seamless handoff
boolean launcherVisible = !finishedToApp || launcherIsVisible;
boolean launcherVisible = !finishedToApp;
updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, false);
updateStateForFlag(FLAG_VISIBLE, launcherVisible);
applyState();
@@ -1134,24 +824,10 @@ public class TaskbarLauncherStateController {
if (DEBUG) {
Log.d(TAG, "endGestureStateOverride - FLAG_IN_APP: " + finishedToApp);
}
controller.updateStateForFlag(FLAG_IN_APP, finishedToApp && !launcherIsVisible);
controller.updateStateForFlag(FLAG_IN_APP, finishedToApp);
controller.applyState();
}
/**
* Helper function to run a callback on the RecentsWindowManager (if it exists).
*/
private void runForRecentsWindowManager(Consumer<RecentsWindowManager> callback) {
if (RecentsWindowFlags.getEnableOverviewInWindow()) {
final TaskbarActivityContext taskbarContext = mControllers.taskbarActivityContext;
RecentsWindowManager recentsWindowManager = RecentsDisplayModel.getINSTANCE()
.get(taskbarContext).getRecentsWindowManager(taskbarContext.getDisplayId());
if (recentsWindowManager != null) {
callback.accept(recentsWindowManager);
}
}
}
private static String getStateString(int flags) {
StringJoiner result = new StringJoiner("|");
appendFlag(result, flags, FLAG_VISIBLE, "flag_visible");
@@ -1175,9 +851,8 @@ public class TaskbarLauncherStateController {
pw.println(String.format(
"%s\tmTaskbarBackgroundAlpha=%.2f", prefix, mTaskbarBackgroundAlpha.value));
pw.println(String.format(
"%s\tmTaskbarAlphaForHome=%.2f", prefix, mTaskbarAlphaForHome.getValue()));
pw.println(String.format("%s\tmPrevState=%s", prefix,
mPrevState == null ? null : getStateString(mPrevState)));
"%s\tmIconAlphaForHome=%.2f", prefix, mIconAlphaForHome.getValue()));
pw.println(String.format("%s\tmPrevState=%s", prefix, getStateString(mPrevState)));
pw.println(String.format("%s\tmState=%s", prefix, getStateString(mState)));
pw.println(String.format("%s\tmLauncherState=%s", prefix, mLauncherState));
pw.println(String.format(
File diff suppressed because it is too large Load Diff
@@ -15,10 +15,12 @@
*/
package com.android.launcher3.taskbar;
import static com.android.window.flags2.Flags.enableDesktopWindowingMode;
import static com.android.window.flags2.Flags.enableDesktopWindowingTaskbarRunningApps;
import android.util.SparseArray;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import com.android.launcher3.LauncherSettings.Favorites;
@@ -26,6 +28,8 @@ import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
@@ -33,12 +37,14 @@ import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.LauncherBindableItemsContainer;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.RecentsModel;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -48,7 +54,7 @@ import java.util.function.Predicate;
* Launcher model Callbacks for rendering taskbar.
*/
public class TaskbarModelCallbacks implements
BgDataModel.Callbacks, LauncherBindableItemsContainer {
BgDataModel.Callbacks, LauncherBindableItemsContainer, RecentsModel.RunningTasksListener {
private final SparseArray<ItemInfo> mHotseatItems = new SparseArray<>();
private List<ItemInfo> mPredictedItems = Collections.emptyList();
@@ -62,7 +68,8 @@ public class TaskbarModelCallbacks implements
// Used to defer any UI updates during the SUW unstash animation.
private boolean mDeferUpdatesForSUW;
private Runnable mDeferredUpdates;
private boolean mBindingItems = false;
private final DesktopVisibilityController.DesktopVisibilityListener mDesktopVisibilityListener =
visible -> updateRunningApps();
public TaskbarModelCallbacks(
TaskbarActivityContext context, TaskbarView container) {
@@ -72,18 +79,51 @@ public class TaskbarModelCallbacks implements
public void init(TaskbarControllers controllers) {
mControllers = controllers;
if (mControllers.taskbarRecentAppsController.getCanShowRunningApps()) {
RecentsModel.INSTANCE.get(mContext).registerRunningTasksListener(this);
if (shouldShowRunningAppsInDesktopMode()) {
DesktopVisibilityController desktopVisibilityController =
LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
if (desktopVisibilityController != null) {
desktopVisibilityController.registerDesktopVisibilityListener(
mDesktopVisibilityListener);
}
}
}
}
/**
* Unregisters listeners in this class.
*/
public void unregisterListeners() {
RecentsModel.INSTANCE.get(mContext).unregisterRunningTasksListener();
if (shouldShowRunningAppsInDesktopMode()) {
DesktopVisibilityController desktopVisibilityController =
LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
if (desktopVisibilityController != null) {
desktopVisibilityController.unregisterDesktopVisibilityListener(
mDesktopVisibilityListener);
}
}
}
private boolean shouldShowRunningAppsInDesktopMode() {
// TODO(b/335401172): unify DesktopMode checks in Launcher
return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps();
}
@Override
public void startBinding() {
mBindingItems = true;
mContext.setBindingItems(true);
mHotseatItems.clear();
mPredictedItems = Collections.emptyList();
}
@Override
public void finishBindingItems(IntSet pagesBoundFirst) {
mBindingItems = false;
mContext.setBindingItems(false);
commitItemsToUI();
}
@@ -115,21 +155,26 @@ public class TaskbarModelCallbacks implements
return modified;
}
@Override
public void bindItemsUpdated(Set<ItemInfo> updates) {
updateContainerItems(updates, mContext);
public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) {
updateWorkspaceItems(updated, mContext);
}
@Override
public View mapOverItems(@NonNull ItemOperator op) {
public void bindRestoreItemsChange(HashSet<ItemInfo> updates) {
updateRestoreItems(updates, mContext);
}
@Override
public void mapOverItems(ItemOperator op) {
final int itemCount = mContainer.getChildCount();
for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
View item = mContainer.getChildAt(itemIdx);
if (item.getTag() instanceof ItemInfo itemInfo && op.evaluate(itemInfo, item)) {
return item;
if (op.evaluate((ItemInfo) item.getTag(), item)) {
return;
}
}
return null;
}
@Override
@@ -170,7 +215,7 @@ public class TaskbarModelCallbacks implements
}
private void commitItemsToUI() {
if (mBindingItems) {
if (mContext.isBindingItems()) {
return;
}
@@ -187,26 +232,26 @@ public class TaskbarModelCallbacks implements
predictionNextIndex++;
}
}
final TaskbarRecentAppsController recentAppsController =
mControllers.taskbarRecentAppsController;
hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos);
hotseatItemInfos = mControllers.taskbarRecentAppsController
.updateHotseatItemInfos(hotseatItemInfos);
Set<String> runningPackages = mControllers.taskbarRecentAppsController.getRunningApps();
Set<String> minimizedPackages = mControllers.taskbarRecentAppsController.getMinimizedApps();
if (mDeferUpdatesForSUW) {
ItemInfo[] finalHotseatItemInfos = hotseatItemInfos;
mDeferredUpdates = () ->
commitHotseatItemUpdates(finalHotseatItemInfos,
recentAppsController.getShownTasks());
commitHotseatItemUpdates(finalHotseatItemInfos, runningPackages,
minimizedPackages);
} else {
commitHotseatItemUpdates(hotseatItemInfos, recentAppsController.getShownTasks());
commitHotseatItemUpdates(hotseatItemInfos, runningPackages, minimizedPackages);
}
}
private void commitHotseatItemUpdates(
ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
mContainer.updateItems(hotseatItemInfos, recentTasks);
mControllers.taskbarViewController.updateIconViewsRunningStates();
mControllers.taskbarPopupController.setHotseatInfosList(mHotseatItems);
private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, Set<String> runningPackages,
Set<String> minimizedPackages) {
mContainer.updateHotseatItems(hotseatItemInfos);
mControllers.taskbarViewController.updateIconViewsRunningStates(runningPackages,
minimizedPackages);
}
/**
@@ -225,11 +270,21 @@ public class TaskbarModelCallbacks implements
}
}
@Override
public void onRunningTasksChanged() {
updateRunningApps();
}
/** Called when there's a change in running apps to update the UI. */
public void commitRunningAppsToUI() {
commitItemsToUI();
}
/** Call TaskbarRecentAppsController to update running apps with mHotseatItems. */
public void updateRunningApps() {
mControllers.taskbarRecentAppsController.updateRunningApps();
}
@Override
public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) {
mControllers.taskbarPopupController.setDeepShortcutMap(deepShortcutMapCopy);
@@ -241,7 +296,7 @@ public class TaskbarModelCallbacks implements
Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
Preconditions.assertUIThread();
mControllers.taskbarAllAppsController.setApps(apps, flags, packageUserKeytoUidMap);
mControllers.taskbarPopupController.setApps(apps);
mControllers.taskbarRecentAppsController.setApps(apps);
}
protected void dumpLogs(String prefix, PrintWriter pw) {
@@ -16,9 +16,6 @@
package com.android.launcher3.taskbar;
import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.ACTION_UP;
import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS;
@@ -27,24 +24,19 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_TAP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_TAP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.Flags;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
@@ -56,8 +48,7 @@ import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskUtils;
import com.android.quickstep.util.ContextualSearchInvoker;
import com.android.systemui.contextualeducation.GestureType;
import com.android.quickstep.util.AssistUtils;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import java.io.PrintWriter;
@@ -79,7 +70,6 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa
private long mLastScreenPinLongPress;
private boolean mScreenPinned;
private boolean mAssistantLongPressEnabled;
private int mLastSentBackAction = ACTION_UP;
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
@@ -87,8 +77,6 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa
pw.println(prefix + "\tmLastScreenPinLongPress=" + mLastScreenPinLongPress);
pw.println(prefix + "\tmScreenPinned=" + mScreenPinned);
pw.println(prefix + "\tmLastSentBackAction="
+ KeyEvent.actionToString(mLastSentBackAction));
}
@Retention(RetentionPolicy.SOURCE)
@@ -120,7 +108,7 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa
private final TaskbarNavButtonCallbacks mCallbacks;
private final SystemUiProxy mSystemUiProxy;
private final Handler mHandler;
private final ContextualSearchInvoker mContextualSearchInvoker;
private final AssistUtils mAssistUtils;
@Nullable private StatsLogManager mStatsLogManager;
private final Runnable mResetLongPress = this::resetScreenUnpin;
@@ -130,48 +118,36 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa
TaskbarNavButtonCallbacks callbacks,
SystemUiProxy systemUiProxy,
Handler handler,
ContextualSearchInvoker contextualSearchInvoker) {
AssistUtils assistUtils) {
mContext = context;
mCallbacks = callbacks;
mSystemUiProxy = systemUiProxy;
mHandler = handler;
mContextualSearchInvoker = contextualSearchInvoker;
mAssistUtils = assistUtils;
}
public void onButtonClick(@TaskbarButton int buttonType, View view) {
if (buttonType == BUTTON_SPACE) {
return;
}
boolean predictiveBackThreeButtonNav;
try {
predictiveBackThreeButtonNav = predictiveBackThreeButtonNav();
} catch (Throwable t) {
predictiveBackThreeButtonNav = false;
}
if (predictiveBackThreeButtonNav && mLastSentBackAction == ACTION_DOWN) {
Log.i(TAG, "Button click ignored while back button is pressed");
// prevent interactions with other buttons while back button is pressed
return;
}
// Provide the same haptic feedback that the system offers for virtual keys.
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
switch (buttonType) {
case BUTTON_BACK:
executeBack(/* keyEvent */ null);
logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
executeBack();
break;
case BUTTON_HOME:
logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_TAP);
mSystemUiProxy.updateContextualEduStats(/* isTrackpadGesture= */ false,
GestureType.HOME);
navigateHome();
break;
case BUTTON_RECENTS:
logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP);
mSystemUiProxy.updateContextualEduStats(/* isTrackpadGesture= */ false,
GestureType.OVERVIEW);
navigateToOverview();
break;
case BUTTON_IME_SWITCH:
logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP);
onImeSwitcherPress();
showIMESwitcher();
break;
case BUTTON_A11Y:
logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_TAP);
@@ -190,27 +166,8 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa
if (buttonType == BUTTON_SPACE) {
return false;
}
boolean predictiveBackThreeButtonNav;
try {
predictiveBackThreeButtonNav = predictiveBackThreeButtonNav();
} catch (Throwable t) {
predictiveBackThreeButtonNav = false;
}
if (predictiveBackThreeButtonNav && mLastSentBackAction == ACTION_DOWN
&& buttonType != BUTTON_BACK && buttonType != BUTTON_RECENTS) {
// prevent interactions with other buttons while back button is pressed (except back
// and recents button for screen-unpin action).
Log.i(TAG, "Button long click ignored while back button is pressed");
return false;
}
// Provide the same haptic feedback that the system offers for long press.
// The haptic feedback from long pressing on the home button is handled by circle to search.
// There are no haptics for long pressing the back button if predictive back is enabled
if (buttonType != BUTTON_HOME
&& (!predictiveBackThreeButtonNav || buttonType != BUTTON_BACK)) {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
// Provide the same haptic feedback that the system offers for virtual keys.
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
switch (buttonType) {
case BUTTON_HOME:
logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS);
@@ -222,19 +179,11 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa
return true;
case BUTTON_BACK:
logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS);
backRecentsLongpress(buttonType);
return true;
return backRecentsLongpress(buttonType);
case BUTTON_RECENTS:
logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS);
backRecentsLongpress(buttonType);
return true;
return backRecentsLongpress(buttonType);
case BUTTON_IME_SWITCH:
if (Flags.imeSwitcherRevamp()) {
logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS);
onImeSwitcherLongPress();
return true;
}
return false;
default:
return false;
}
@@ -304,10 +253,6 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa
}
private void resetScreenUnpin() {
// if only back button was long pressed, navigate back like a single click back behavior.
if (mLongPressedButtons == BUTTON_BACK) {
executeBack(null);
}
mLongPressedButtons = 0;
mLastScreenPinLongPress = 0;
}
@@ -350,41 +295,14 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa
mCallbacks.onToggleOverview();
}
public void hideOverview() {
mCallbacks.onHideOverview();
private void executeBack() {
mSystemUiProxy.onBackPressed();
}
void sendBackKeyEvent(int action, boolean cancelled) {
if (action == mLastSentBackAction) {
// There must always be an alternating sequence of ACTION_DOWN and ACTION_UP events
return;
}
long time = SystemClock.uptimeMillis();
KeyEvent keyEvent = new KeyEvent(time, time, action, KeyEvent.KEYCODE_BACK, 0);
if (cancelled) {
keyEvent.cancel();
}
executeBack(keyEvent);
}
private void executeBack(@Nullable KeyEvent keyEvent) {
if (keyEvent == null || (keyEvent.getAction() == ACTION_UP && !keyEvent.isCanceled())) {
logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
mSystemUiProxy.updateContextualEduStats(/* isTrackpadGesture= */ false,
GestureType.BACK);
}
mSystemUiProxy.onBackEvent(keyEvent);
mLastSentBackAction = keyEvent != null ? keyEvent.getAction() : ACTION_UP;
}
private void onImeSwitcherPress() {
private void showIMESwitcher() {
mSystemUiProxy.onImeSwitcherPressed();
}
private void onImeSwitcherLongPress() {
mSystemUiProxy.onImeSwitcherLongPress();
}
private void notifyA11yClick(boolean longClick) {
if (longClick) {
mSystemUiProxy.notifyAccessibilityButtonLongClicked();
@@ -397,9 +315,8 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa
if (mScreenPinned || !mAssistantLongPressEnabled) {
return;
}
// Attempt to start Contextual Search, otherwise fall back to SysUi's implementation.
if (!mContextualSearchInvoker.tryStartAssistOverride(
INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) {
// Attempt to start Assist with AssistUtils, otherwise fall back to SysUi's implementation.
if (!mAssistUtils.tryStartAssistOverride(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) {
Bundle args = new Bundle();
args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
mSystemUiProxy.startAssistant(args);
@@ -421,8 +338,5 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa
/** Callback invoked when the overview button is pressed. */
default void onToggleOverview() {}
/** Callback invoken when a visible overview needs to be hidden. */
default void onHideOverview() { }
}
}
@@ -30,11 +30,12 @@ import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_UNPINNED
import com.android.launcher3.taskbar.TaskbarDividerPopupView.Companion.createAndPopulate
import java.io.PrintWriter
import kotlin.jvm.optionals.getOrNull
/** Controls taskbar pinning through a popup view. */
class TaskbarPinningController(private val context: TaskbarActivityContext) :
TaskbarControllers.LoggableTaskbarController {
class TaskbarPinningController(
private val context: TaskbarActivityContext,
private val isInDesktopModeProvider: () -> Boolean,
) : TaskbarControllers.LoggableTaskbarController {
private lateinit var controllers: TaskbarControllers
private lateinit var taskbarSharedState: TaskbarSharedState
@@ -57,11 +58,7 @@ class TaskbarPinningController(private val context: TaskbarActivityContext) :
return
}
val shouldPinTaskbar =
if (
controllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview(
context.displayId
)
) {
if (isInDesktopModeProvider()) {
!launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)
} else {
!launcherPrefs.get(TASKBAR_PINNING)
@@ -81,10 +78,10 @@ class TaskbarPinningController(private val context: TaskbarActivityContext) :
}
}
fun showPinningView(view: View, horizontalPosition: Float = -1f) {
fun showPinningView(view: View) {
context.isTaskbarWindowFullscreen = true
view.post {
val popupView = getPopupView(view, horizontalPosition)
val popupView = getPopupView(view)
popupView.requestFocus()
popupView.onCloseCallback = onCloseCallback
context.onPopupVisibilityChanged(true)
@@ -94,8 +91,8 @@ class TaskbarPinningController(private val context: TaskbarActivityContext) :
}
@VisibleForTesting
fun getPopupView(view: View, horizontalPosition: Float = -1f): TaskbarDividerPopupView<*> {
return createAndPopulate(view, context, horizontalPosition)
fun getPopupView(view: View): TaskbarDividerPopupView<*> {
return createAndPopulate(view, context)
}
@VisibleForTesting
@@ -122,13 +119,9 @@ class TaskbarPinningController(private val context: TaskbarActivityContext) :
dragLayerController.taskbarBackgroundProgress.animateToValue(animateToValue),
taskbarViewController.taskbarIconTranslationYForPinning.animateToValue(animateToValue),
taskbarViewController.taskbarIconScaleForPinning.animateToValue(animateToValue),
taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue),
taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue)
)
controllers.bubbleControllers.getOrNull()?.bubbleBarViewController?.let {
// if bubble bar is not visible no need to add it`s animations
if (!it.isBubbleBarVisible) return@let
animatorSet.playTogether(it.bubbleBarPinning.animateToValue(animateToValue))
}
animatorSet.interpolator = Interpolators.EMPHASIZED
return animatorSet
}
@@ -141,14 +134,10 @@ class TaskbarPinningController(private val context: TaskbarActivityContext) :
@VisibleForTesting
fun recreateTaskbarAndUpdatePinningValue() {
updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(false)
if (
controllers.taskbarDesktopModeController.isInDesktopModeAndNotInOverview(
context.displayId
)
) {
if (isInDesktopModeProvider()) {
launcherPrefs.put(
TASKBAR_PINNING_IN_DESKTOP_MODE,
!launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE),
!launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)
)
} else {
launcherPrefs.put(TASKBAR_PINNING, !launcherPrefs.get(TASKBAR_PINNING))
@@ -15,29 +15,26 @@
*/
package com.android.launcher3.taskbar;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
import static com.android.launcher3.model.data.AppInfo.COMPONENT_KEY_COMPARATOR;
import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.graphics.Point;
import android.util.Pair;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.logging.InstanceId;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.Flags;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.dot.FolderDotInfo;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.notification.NotificationListener;
@@ -47,21 +44,19 @@ import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.launcher3.splitscreen.SplitShortcut;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.LauncherBindableItemsContainer;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.ShortcutUtil;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.views.ActivityContext;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.LogUtils;
import com.android.quickstep.util.SingleTask;
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -74,24 +69,16 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
private static final SystemShortcut.Factory<BaseTaskbarContext>
APP_INFO = SystemShortcut.AppInfo::new;
private static final SystemShortcut.Factory<BaseTaskbarContext>
BUBBLE = SystemShortcut.BubbleShortcut::new;
private final TaskbarActivityContext mContext;
private final PopupDataProvider mPopupDataProvider;
// Initialized in init.
private TaskbarControllers mControllers;
private boolean mAllowInitialSplitSelection;
private AppInfo[] mAppInfosList = AppInfo.EMPTY_ARRAY;
// Saves the ItemInfos in the hotseat without the predicted items.
private SparseArray<ItemInfo> mHotseatInfosList;
private ManageWindowsTaskbarShortcut<BaseTaskbarContext> mManageWindowsTaskbarShortcut;
public TaskbarPopupController(TaskbarActivityContext context) {
mContext = context;
mPopupDataProvider = new PopupDataProvider(mContext);
mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
}
public void init(TaskbarControllers controllers) {
@@ -113,23 +100,43 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy);
}
/** Closes the multi-instance menu if it is enabled and currently open. */
public void maybeCloseMultiInstanceMenu() {
if (Flags.enableMultiInstanceMenuTaskbar() && mManageWindowsTaskbarShortcut != null) {
mManageWindowsTaskbarShortcut.closeMultiInstanceMenu();
cleanUpMultiInstanceMenuReference();
}
}
/** Releases the reference to the Taskbar multi-instance menu */
public void cleanUpMultiInstanceMenuReference() {
mManageWindowsTaskbarShortcut = null;
}
public void setAllowInitialSplitSelection(boolean allowInitialSplitSelection) {
mAllowInitialSplitSelection = allowInitialSplitSelection;
}
private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
final PackageUserKey packageUserKey = new PackageUserKey(null, null);
Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
|| updatedDots.test(packageUserKey);
LauncherBindableItemsContainer.ItemOperator op = (info, v) -> {
if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
if (matcher.test(info)) {
((BubbleTextView) v).applyDotState(info, true /* animate */);
}
} else if (info instanceof FolderInfo && v instanceof FolderIcon) {
FolderInfo fi = (FolderInfo) info;
if (fi.anyMatch(matcher)) {
FolderDotInfo folderDotInfo = new FolderDotInfo();
for (ItemInfo si : fi.getContents()) {
folderDotInfo.addDotInfo(mPopupDataProvider.getDotInfoForItem(si));
}
((FolderIcon) v).setDotInfo(folderDotInfo);
}
}
// process all the shortcuts
return false;
};
mControllers.taskbarViewController.mapOverItems(op);
Folder folder = Folder.getOpen(mContext);
if (folder != null) {
folder.iterateOverItems(op);
}
mControllers.taskbarAllAppsController.updateNotificationDots(updatedDots);
}
/**
* Shows the notifications and deep shortcuts associated with a Taskbar {@param icon}.
* @return the container if shown or null.
@@ -141,35 +148,22 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
icon.clearFocus();
return null;
}
ItemInfo itemInfo;
if (icon.getTag() instanceof ItemInfo item && ShortcutUtil.supportsShortcuts(item)) {
itemInfo = item;
} else if (icon.getTag() instanceof SingleTask task) {
itemInfo = SingleTask.Companion.createTaskItemInfo(task);
} else {
ItemInfo item = (ItemInfo) icon.getTag();
if (!ShortcutUtil.supportsShortcuts(item)) {
return null;
}
PopupContainerWithArrow<BaseTaskbarContext> container;
int deepShortcutCount = mPopupDataProvider.getShortcutCountForItem(itemInfo);
int deepShortcutCount = mPopupDataProvider.getShortcutCountForItem(item);
// TODO(b/198438631): add support for INSTALL shortcut factory
List<SystemShortcut> systemShortcuts = getSystemShortcuts()
.map(s -> s.getShortcut(context, itemInfo, icon))
.map(s -> s.getShortcut(context, item, icon))
.filter(Objects::nonNull)
.collect(Collectors.toList());
// TODO(b/375648361): Revisit to see if this can be implemented within getSystemShortcuts().
if (Flags.enablePinningAppWithContextMenu()) {
SystemShortcut shortcut = createPinShortcut(context, itemInfo, icon);
if (shortcut != null) {
systemShortcuts.add(0, shortcut);
}
}
container = (PopupContainerWithArrow) context.getLayoutInflater().inflate(
R.layout.popup_container, context.getDragLayer(), false);
container.populateAndShowRows(icon, itemInfo, deepShortcutCount, systemShortcuts);
container.populateAndShowRows(icon, deepShortcutCount, systemShortcuts);
// TODO (b/198438631): configure for taskbar/context
container.setPopupItemDragHandler(new TaskbarPopupItemDragHandler());
@@ -187,45 +181,11 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
// Create a Stream of all applicable system shortcuts
private Stream<SystemShortcut.Factory> getSystemShortcuts() {
// append split options to APP_INFO shortcut if not in Desktop Windowing mode, the order
// here will reflect in the popup
ArrayList<SystemShortcut.Factory> shortcuts = new ArrayList<>();
shortcuts.add(APP_INFO);
if (!mControllers.taskbarDesktopModeController
.isInDesktopModeAndNotInOverview(mContext.getDisplayId())) {
shortcuts.addAll(mControllers.uiController.getSplitMenuOptions().toList());
}
if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
shortcuts.add(BUBBLE);
}
if (Flags.enableMultiInstanceMenuTaskbar()
&& DesktopModeStatus.canEnterDesktopMode(mContext)
&& !mControllers.taskbarStashController.isInOverview()) {
maybeCloseMultiInstanceMenu();
shortcuts.addAll(getMultiInstanceMenuOptions().toList());
}
return shortcuts.stream();
}
@Nullable
private SystemShortcut createPinShortcut(BaseTaskbarContext target, ItemInfo itemInfo,
BubbleTextView originalView) {
// Predicted items use {@code HotseatPredictionController.PinPrediction} shortcut to pin.
if (itemInfo.isPredictedItem()) {
return null;
}
if (itemInfo.container == CONTAINER_HOTSEAT) {
return new PinToTaskbarShortcut<>(target, itemInfo, originalView, false,
mHotseatInfosList);
}
if (mHotseatInfosList.size()
< mContext.getTaskbarSpecsEvaluator().getNumShownHotseatIcons()) {
return new PinToTaskbarShortcut<>(target, itemInfo, originalView, true,
mHotseatInfosList);
}
return null;
// append split options to APP_INFO shortcut, the order here will reflect in the popup
return Stream.concat(
Stream.of(APP_INFO),
mControllers.uiController.getSplitMenuOptions()
);
}
@Override
@@ -288,83 +248,7 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
originalView, position, mAllowInitialSplitSelection);
}
/**
* Set the list of AppInfos to be able to pull from later
*/
public void setApps(AppInfo[] apps) {
mAppInfosList = apps;
}
/**
* Finds and returns an AppInfo object from a list, using its ComponentKey for identification.
* Based off of {@link com.android.launcher3.allapps.AllAppsStore#getApp(ComponentKey)}
* since we cannot access AllAppsStore from here.
*/
public AppInfo getApp(ComponentKey key) {
if (key == null) {
return null;
}
AppInfo tempInfo = new AppInfo();
tempInfo.componentName = key.componentName;
tempInfo.user = key.user;
int index = Arrays.binarySearch(mAppInfosList, tempInfo, COMPONENT_KEY_COMPARATOR);
return index < 0 ? null : mAppInfosList[index];
}
public void setHotseatInfosList(SparseArray<ItemInfo> info) {
mHotseatInfosList = info;
}
/**
* Returns a stream of Multi Instance menu options if an app supports it.
*/
Stream<SystemShortcut.Factory<BaseTaskbarContext>> getMultiInstanceMenuOptions() {
SystemShortcut.Factory<BaseTaskbarContext> f1 = createNewWindowShortcutFactory();
SystemShortcut.Factory<BaseTaskbarContext> f2 = createManageWindowsShortcutFactory();
return f1 != null ? Stream.of(f1, f2) : Stream.empty();
}
/**
* Creates a factory function representing a "New Window" menu item only if the calling app
* supports multi-instance.
* @return A factory function to be used in populating the long-press menu.
*/
SystemShortcut.Factory<BaseTaskbarContext> createNewWindowShortcutFactory() {
return (context, itemInfo, originalView) -> {
if (shouldShowMultiInstanceOptions(itemInfo)) {
return new NewWindowTaskbarShortcut<>(context, itemInfo, originalView);
}
return null;
};
}
/**
* Creates a factory function representing a "Manage Windows" menu item only if the calling app
* supports multi-instance. This menu item shows the open instances of the calling app.
* @return A factory function to be used in populating the long-press menu.
*/
public SystemShortcut.Factory<BaseTaskbarContext> createManageWindowsShortcutFactory() {
return (context, itemInfo, originalView) -> {
if (shouldShowMultiInstanceOptions(itemInfo)) {
mManageWindowsTaskbarShortcut = new ManageWindowsTaskbarShortcut<>(
context, itemInfo, originalView, mControllers);
return mManageWindowsTaskbarShortcut;
}
return null;
};
}
/**
* Determines whether to show multi-instance options for a given item.
*/
private boolean shouldShowMultiInstanceOptions(ItemInfo itemInfo) {
ComponentKey key = itemInfo.getComponentKey();
AppInfo app = getApp(key);
return app != null && app.supportsMultiInstance()
&& itemInfo.container != CONTAINER_ALL_APPS;
}
/**
/**
* A single menu item ("Split left," "Split right," or "Split top") that executes a split
* from the taskbar, as if the user performed a drag and drop split.
* Includes an onClick method that initiates the actual split.
@@ -15,23 +15,18 @@
*/
package com.android.launcher3.taskbar
import android.content.Context
import android.window.DesktopModeFlags
import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration
import androidx.annotation.VisibleForTesting
import com.android.launcher3.BubbleTextView.RunningAppState
import com.android.launcher3.Flags
import com.android.launcher3.Flags.enableRecentsInTaskbar
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.model.data.TaskItemInfo
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
import com.android.launcher3.util.CancellableTask
import com.android.quickstep.RecentsFilterState
import com.android.quickstep.RecentsModel
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
import com.android.quickstep.util.SingleTask
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.window.flags2.Flags.enableDesktopWindowingMode
import com.android.window.flags2.Flags.enableDesktopWindowingTaskbarRunningApps
import java.io.PrintWriter
/**
@@ -39,406 +34,153 @@ import java.io.PrintWriter
* - When in Fullscreen mode: show the N most recent Tasks
* - When in Desktop Mode: show the currently running (open) Tasks
*/
class TaskbarRecentAppsController(context: Context, private val recentsModel: RecentsModel) :
LoggableTaskbarController {
class TaskbarRecentAppsController(
private val recentsModel: RecentsModel,
// Pass a provider here instead of the actual DesktopVisibilityController instance since that
// instance might not be available when this constructor is called.
private val desktopVisibilityControllerProvider: () -> DesktopVisibilityController?,
) : LoggableTaskbarController {
var canShowRunningApps =
DesktopModeStatus.canEnterDesktopMode(context) &&
DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS.isTrue
@VisibleForTesting
set(isEnabledFromTest) {
field = isEnabledFromTest
if (!field && !canShowRecentApps) {
recentsModel.unregisterRecentTasksChangedListener()
}
}
// TODO(b/335401172): unify DesktopMode checks in Launcher.
val canShowRunningApps =
enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps()
// TODO(b/343532825): Add a setting to disable Recents even when the flag is on.
var canShowRecentApps = enableRecentsInTaskbar()
var isEnabled: Boolean = enableRecentsInTaskbar() || canShowRunningApps
@VisibleForTesting
set(isEnabledFromTest) {
set(isEnabledFromTest){
field = isEnabledFromTest
if (!field && !canShowRunningApps) {
recentsModel.unregisterRecentTasksChangedListener()
}
}
// Initialized in init.
private lateinit var controllers: TaskbarControllers
var shownHotseatItems: List<ItemInfo> = emptyList()
private set
private var apps: Array<AppInfo>? = null
private var allRunningDesktopAppInfos: List<AppInfo>? = null
private var allMinimizedDesktopAppInfos: List<AppInfo>? = null
private var allRecentTasks: List<GroupTask> = emptyList()
private var desktopTask: DesktopTask? = null
// Keeps track of the order in which running tasks appear.
private var orderedRunningTaskIds = emptyList<Int>()
var shownTasks: List<GroupTask> = emptyList()
private set
private val desktopVisibilityController: DesktopVisibilityController?
get() = desktopVisibilityControllerProvider()
val shownTaskIds: List<Int>
get() = shownTasks.flatMap { shownTask -> shownTask.tasks }.map { it.key.id }
private val isInDesktopMode: Boolean
get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
/**
* The task-state of an app, i.e. whether the app has a task and what state that task is in.
*
* @property taskId The ID of the task if one exists (i.e. if the state is RUNNING or
* MINIMIZED), null otherwise (NOT_RUNNING).
*/
data class TaskState(val runningAppState: RunningAppState, val taskId: Int? = null)
/**
* Returns the state of the most active Desktop task represented by the given [ItemInfo].
*
* If there are several tasks represented by the same [ItemInfo] we return the most active one,
* i.e. we return [DesktopAppState.RUNNING] over [DesktopAppState.MINIMIZED], and
* [DesktopAppState.MINIMIZED] over [DesktopAppState.NOT_RUNNING].
*/
fun getDesktopItemState(itemInfo: ItemInfo?): TaskState {
val packageName =
itemInfo?.getTargetPackage() ?: return TaskState(RunningAppState.NOT_RUNNING)
return getDesktopTaskState(packageName, itemInfo.user.identifier)
}
private fun getDesktopTaskState(packageName: String, userId: Int): TaskState {
val tasks = desktopTask?.tasks ?: return TaskState(RunningAppState.NOT_RUNNING)
val appTasks =
tasks.filter { task ->
packageName == task.key.packageName && task.key.userId == userId
}
val runningTask = appTasks.find { getRunningAppState(it.key.id) == RunningAppState.RUNNING }
if (runningTask != null) {
return TaskState(RunningAppState.RUNNING, runningTask.key.id)
}
val minimizedTask =
appTasks.find { getRunningAppState(it.key.id) == RunningAppState.MINIMIZED }
if (minimizedTask != null) {
return TaskState(RunningAppState.MINIMIZED, taskId = minimizedTask.key.id)
}
return TaskState(RunningAppState.NOT_RUNNING)
}
/** Get the [RunningAppState] for the given task. */
fun getRunningAppState(taskId: Int): RunningAppState {
return when (taskId) {
in minimizedTaskIds -> RunningAppState.MINIMIZED
in runningTaskIds -> RunningAppState.RUNNING
else -> RunningAppState.NOT_RUNNING
}
}
@VisibleForTesting
val runningTaskIds: Set<Int>
/**
* Returns the task IDs of apps that should be indicated as "running" to the user.
* Specifically, we return all the open tasks if we are in Desktop mode, else emptySet().
*/
val runningApps: Set<String>
get() {
if (
!canShowRunningApps ||
!controllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar()
) {
if (!isEnabled || !isInDesktopMode) {
return emptySet()
}
val tasks = desktopTask?.tasks ?: return emptySet()
return tasks.map { task -> task.key.id }.toSet()
return allRunningDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() ?: emptySet()
}
@VisibleForTesting
val minimizedTaskIds: Set<Int>
/**
* Returns the task IDs for the tasks that should be indicated as "minimized" to the user.
*/
val minimizedApps: Set<String>
get() {
if (
!canShowRunningApps ||
!controllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar()
) {
if (!isInDesktopMode) {
return emptySet()
}
val desktopTasks = desktopTask?.tasks ?: return emptySet()
return desktopTasks.filter { !it.isVisible }.map { task -> task.key.id }.toSet()
return allMinimizedDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet()
?: emptySet()
}
private val recentTasksChangedListener =
RecentsModel.RecentTasksChangedListener { reloadRecentTasksIfNeeded() }
private val iconLoadRequests: MutableSet<CancellableTask<*>> = HashSet()
// TODO(b/343291428): add TaskVisualsChangListener as well (for calendar/clock?)
// Used to keep track of the last requested task list ID, so that we do not request to load the
// tasks again if we have already requested it and the task list has not changed
private var taskListChangeId = -1
fun init(taskbarControllers: TaskbarControllers) {
controllers = taskbarControllers
if (canShowRunningApps || canShowRecentApps) {
recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener)
controllers.runAfterInit { reloadRecentTasksIfNeeded() }
}
}
fun onDestroy() {
recentsModel.unregisterRecentTasksChangedListener()
iconLoadRequests.forEach { it.cancel() }
iconLoadRequests.clear()
apps = null
}
/** Stores the current [AppInfo] instances, no-op except in desktop environment. */
fun setApps(apps: Array<AppInfo>?) {
this.apps = apps
}
/** Called to update hotseatItems, in order to de-dupe them from Recent/Running tasks later. */
// TODO(next CL): add new section of Tasks instead of changing Hotseat items
fun updateHotseatItemInfos(hotseatItems: Array<ItemInfo?>): Array<ItemInfo?> {
// Ignore predicted apps - we show running or recent apps instead.
val showDesktopTasks =
controllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar()
val removePredictions =
(showDesktopTasks && canShowRunningApps) || (!showDesktopTasks && canShowRecentApps)
if (!removePredictions) {
shownHotseatItems = hotseatItems.filterNotNull()
onRecentsOrHotseatChanged()
if (!isEnabled || !isInDesktopMode) {
return hotseatItems
}
shownHotseatItems =
val newHotseatItemInfos =
hotseatItems
.filterNotNull()
// Ignore predicted apps - we show running apps instead
.filter { itemInfo -> !itemInfo.isPredictedItem }
.toMutableList()
if (showDesktopTasks && canShowRunningApps) {
shownHotseatItems =
updateHotseatItemsFromRunningTasks(
getOrderedAndWrappedDesktopTasks(),
shownHotseatItems,
)
}
onRecentsOrHotseatChanged()
return shownHotseatItems.toTypedArray()
}
private fun getOrderedAndWrappedDesktopTasks(): List<SingleTask> {
val tasks = desktopTask?.tasks ?: emptyList()
// We wrap each task in the Desktop as a `SingleTask`.
val orderFromId = orderedRunningTaskIds.withIndex().associate { (index, id) -> id to index }
val sortedTasks = tasks.sortedWith(compareBy(nullsLast()) { orderFromId[it.key.id] })
return sortedTasks.map { SingleTask(it) }
}
private fun reloadRecentTasksIfNeeded() {
if (!recentsModel.isTaskListValid(taskListChangeId)) {
taskListChangeId =
recentsModel.getTasks(
{ tasks ->
allRecentTasks = tasks
val oldRunningTaskdIds = runningTaskIds
val oldMinimizedTaskIds = minimizedTaskIds
desktopTask = allRecentTasks.filterIsInstance<DesktopTask>().firstOrNull()
val runningTasksChanged = oldRunningTaskdIds != runningTaskIds
val minimizedTasksChanged = oldMinimizedTaskIds != minimizedTaskIds
if (
onRecentsOrHotseatChanged() ||
runningTasksChanged ||
minimizedTasksChanged
) {
controllers.taskbarViewController.commitRunningAppsToUI()
}
},
RecentsFilterState.EMPTY_FILTER,
)
}
}
/**
* Updates [shownTasks] when Recents or Hotseat changes.
*
* @return Whether [shownTasks] changed.
*/
private fun onRecentsOrHotseatChanged(): Boolean {
val oldShownTasks = shownTasks
orderedRunningTaskIds = updateOrderedRunningTaskIds()
shownTasks =
if (controllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar()) {
computeShownRunningTasks()
} else {
computeShownRecentTasks()
val runningDesktopAppInfos =
allRunningDesktopAppInfos?.let {
getRunningDesktopAppInfosExceptHotseatApps(it, newHotseatItemInfos.toList())
}
val shownTasksChanged = oldShownTasks != shownTasks
if (!shownTasksChanged) {
return shownTasksChanged
if (runningDesktopAppInfos != null) {
newHotseatItemInfos.addAll(runningDesktopAppInfos)
}
for (groupTask in shownTasks) {
for (task in groupTask.tasks) {
val cancellableTask =
recentsModel.iconCache.getIconInBackground(task) {
icon,
contentDescription,
title ->
task.icon = icon
task.titleDescription = contentDescription
task.title = title
controllers.taskbarViewController.onTaskUpdated(task)
}
if (cancellableTask != null) {
iconLoadRequests.add(cancellableTask)
}
}
}
return shownTasksChanged
return newHotseatItemInfos.toTypedArray()
}
private fun updateOrderedRunningTaskIds(): MutableList<Int> {
val desktopTasksAsList = getOrderedAndWrappedDesktopTasks().map { it.task }
val desktopTaskIds = desktopTasksAsList.map { it.key.id }
var newOrder =
orderedRunningTaskIds
.filter { it in desktopTaskIds } // Only keep the tasks that are still running
.toMutableList()
// Add new tasks not already listed
newOrder.addAll(desktopTaskIds.filter { it !in newOrder })
return newOrder
private fun getRunningDesktopAppInfosExceptHotseatApps(
allRunningDesktopAppInfos: List<AppInfo>,
hotseatItems: List<ItemInfo>
): List<ItemInfo> {
val hotseatPackages = hotseatItems.map { it.targetPackage }
return allRunningDesktopAppInfos
.filter { appInfo -> !hotseatPackages.contains(appInfo.targetPackage) }
.map { WorkspaceItemInfo(it) }
}
/**
* Computes the list of running tasks to be shown in the recent apps section of the taskbar in
* desktop mode, taking into account deduplication against hotseat items and existing tasks.
*/
private fun computeShownRunningTasks(): List<GroupTask> {
if (!canShowRunningApps) {
private fun getDesktopRunningTasks(): List<RunningTaskInfo> =
recentsModel.runningTasks.filter { taskInfo: RunningTaskInfo ->
taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM
}
// TODO(b/335398876) fetch app icons from Tasks instead of AppInfos
private fun getAppInfosFromRunningTasks(tasks: List<RunningTaskInfo>): List<AppInfo> {
// Early return if apps is empty, since we then have no AppInfo to compare to
if (apps == null) {
return emptyList()
}
val desktopTasks = getOrderedAndWrappedDesktopTasks()
val newShownTasks =
if (Flags.enableMultiInstanceMenuTaskbar()) {
val deduplicatedDesktopTasks =
desktopTasks.distinctBy { Pair(it.task.key.packageName, it.task.key.userId) }
shownTasks
.filter {
it is SingleTask &&
it.task.key.id in deduplicatedDesktopTasks.map { it.task.key.id }
}
.toMutableList()
.apply {
addAll(
deduplicatedDesktopTasks.filter { currentTask ->
val currentTaskKey = currentTask.task.key
currentTaskKey.id !in shownTaskIds &&
shownHotseatItems.none { hotseatItem ->
currentTask.containsPackage(
hotseatItem.targetPackage,
hotseatItem.user.identifier,
)
}
}
)
}
} else {
val desktopTaskIds = desktopTasks.map { it.task.key.id }
val shownHotseatItemTaskIds =
shownHotseatItems.mapNotNull { it as? TaskItemInfo }.map { it.taskId }
shownTasks
.filter { it is SingleTask && it.task.key.id in desktopTaskIds }
.toMutableList()
.apply {
addAll(
desktopTasks.filter { desktopTask ->
desktopTask.task.key.id !in shownTaskIds
}
)
removeAll { it is SingleTask && it.task.key.id in shownHotseatItemTaskIds }
}
}
return newShownTasks
val packageNames = tasks.map { it.realActivity?.packageName }.distinct().filterNotNull()
return packageNames
.map { packageName -> apps?.find { app -> packageName == app.targetPackage } }
.filterNotNull()
}
private fun computeShownRecentTasks(): List<GroupTask> {
if (!canShowRecentApps || allRecentTasks.isEmpty()) {
return emptyList()
/** Called to update the list of currently running apps, no-op except in desktop environment. */
fun updateRunningApps() {
if (!isEnabled || !isInDesktopMode) {
return controllers.taskbarViewController.commitRunningAppsToUI()
}
// Remove the current task.
val allRecentTasks = allRecentTasks.subList(0, allRecentTasks.size - 1)
var shownTasks = dedupeHotseatTasks(allRecentTasks, shownHotseatItems)
if (shownTasks.size > MAX_RECENT_TASKS) {
// Remove any tasks older than MAX_RECENT_TASKS.
shownTasks = shownTasks.subList(shownTasks.size - MAX_RECENT_TASKS, shownTasks.size)
}
return shownTasks
val runningTasks = getDesktopRunningTasks()
val runningAppInfo = getAppInfosFromRunningTasks(runningTasks)
allRunningDesktopAppInfos = runningAppInfo
updateMinimizedApps(runningTasks, runningAppInfo)
controllers.taskbarViewController.commitRunningAppsToUI()
}
private fun dedupeHotseatTasks(
groupTasks: List<GroupTask>,
shownHotseatItems: List<ItemInfo>,
): List<GroupTask> {
// TODO: b/393476333 - Check the behavior of the Taskbar recents section when empty desks
// become supported.
return if (Flags.enableMultiInstanceMenuTaskbar()) {
groupTasks.filter { groupTask ->
// Keep tasks that are group tasks or unique package name/user combinations
when (groupTask) {
is SingleTask ->
shownHotseatItems.none {
groupTask.containsPackage(it.targetPackage, it.user.identifier)
}
else -> true
private fun updateMinimizedApps(
runningTasks: List<RunningTaskInfo>,
runningAppInfo: List<AppInfo>,
) {
val allRunningAppTasks =
runningAppInfo
.mapNotNull { appInfo -> appInfo.targetPackage?.let { appInfo to it } }
.associate { (appInfo, targetPackage) ->
appInfo to
runningTasks
.filter { it.realActivity?.packageName == targetPackage }
.map { it.taskId }
}
}
} else {
val hotseatPackages = shownHotseatItems.map { it.targetPackage }
groupTasks.filter { groupTask ->
when (groupTask) {
is SingleTask -> hotseatPackages.none { groupTask.containsPackage(it) }
else -> true
}
}
}
val minimizedTaskIds = runningTasks.associate { it.taskId to !it.isVisible }
allMinimizedDesktopAppInfos =
allRunningAppTasks
.filterValues { taskIds -> taskIds.all { minimizedTaskIds[it] ?: false } }
.keys
.toList()
}
/**
* Returns the hotseat items updated so that any item that points to a package+user with a
* running task also references that task.
*/
private fun updateHotseatItemsFromRunningTasks(
groupTasks: List<GroupTask>,
shownHotseatItems: List<ItemInfo>,
): List<ItemInfo> =
shownHotseatItems.map { itemInfo ->
if (itemInfo is TaskItemInfo) {
itemInfo
} else {
val foundTask =
groupTasks
.flatMap { it.tasks }
.find { task ->
task.key.packageName == itemInfo.targetPackage &&
task.key.userId == itemInfo.user.identifier
} ?: return@map itemInfo
TaskItemInfo(foundTask.key.id, itemInfo as WorkspaceItemInfo)
}
}
override fun dumpLogs(prefix: String, pw: PrintWriter) {
pw.println("$prefix TaskbarRecentAppsController:")
pw.println("$prefix\tisEnabled=$isEnabled")
pw.println("$prefix\tcanShowRunningApps=$canShowRunningApps")
pw.println("$prefix\tcanShowRecentApps=$canShowRecentApps")
pw.println("$prefix\tshownHotseatItems=${shownHotseatItems.map{item->item.targetPackage}}")
pw.println("$prefix\tallRecentTasks=${allRecentTasks.map { it.packageNames }}")
pw.println("$prefix\tdesktopTask=${desktopTask?.packageNames}")
pw.println("$prefix\tshownTasks=${shownTasks.map { it.packageNames }}")
pw.println("$prefix\trunningTaskIds=$runningTaskIds")
pw.println("$prefix\tminimizedTaskIds=$minimizedTaskIds")
}
private val GroupTask.packageNames: List<String>
get() = tasks.map { task -> task.key.packageName }
private companion object {
const val MAX_RECENT_TASKS = 2
// TODO(next CL): add more logs
}
}
@@ -20,17 +20,15 @@ import static android.view.View.VISIBLE;
import static com.android.launcher3.taskbar.bubbles.BubbleBarController.isBubbleBarEnabled;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
import static com.android.wm.shell.common.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
import static com.android.wm.shell.shared.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA;
import android.animation.ObjectAnimator;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.taskbar.bubbles.BubbleControllers;
import com.android.launcher3.util.DisplayController;
import com.android.quickstep.SystemUiProxy;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -67,7 +65,6 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa
*/
public void init(TaskbarControllers controllers) {
mControllers = controllers;
onTaskbarVisibilityChanged(mControllers.taskbarViewController.getTaskbarVisibility());
}
/**
@@ -78,7 +75,7 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa
public void onTaskbarVisibilityChanged(int visibility) {
mTaskbarVisible = visibility == VISIBLE;
if (shouldShowScrim()) {
showScrim(true, computeScrimAlpha(), false /* skipAnim */);
showScrim(true, getScrimAlpha(), false /* skipAnim */);
} else if (mScrimView.getScrimAlpha() > 0f) {
showScrim(false, 0, false /* skipAnim */);
}
@@ -88,42 +85,26 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa
* Updates the scrim state based on the flags.
*/
public void updateStateForSysuiFlags(@SystemUiStateFlags long stateFlags, boolean skipAnim) {
if (mActivity.isPhoneMode()) {
// There is no scrim for the bar in the phone mode.
return;
}
boolean isTransient = mActivity.isTransientTaskbar();
if (isBubbleBarEnabled() && isTransient) {
if (isBubbleBarEnabled() && DisplayController.isTransientTaskbar(mActivity)) {
// These scrims aren't used if bubble bar & transient taskbar are active.
return;
}
mSysUiStateFlags = stateFlags;
showScrim(shouldShowScrim(), computeScrimAlpha(), skipAnim);
showScrim(shouldShowScrim(), getScrimAlpha(), skipAnim);
}
private boolean shouldShowScrim() {
final boolean bubblesExpanded = (mSysUiStateFlags & SYSUI_STATE_BUBBLES_EXPANDED) != 0;
boolean isShadeVisible = (mSysUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0;
BubbleControllers bubbleControllers = mActivity.getBubbleControllers();
boolean isBubbleControllersPresented = bubbleControllers != null;
// when the taskbar is in persistent mode, we hide the task bar icons on bubble bar expand,
// which makes the taskbar invisible, so need to check if the bubble bar is not on home
// to show the scrim view
boolean showScrimForBubbles = bubblesExpanded
&& !mTaskbarVisible
&& isBubbleControllersPresented
&& !mActivity.isTransientTaskbar()
&& !bubbleControllers.bubbleStashController.isBubblesShowingOnHome();
return bubblesExpanded && !mControllers.navbarButtonsViewController.isImeVisible()
&& !isShadeVisible
&& !mControllers.taskbarStashController.isStashed()
&& (mTaskbarVisible || showScrimForBubbles)
&& !mControllers.taskbarStashController.isHiddenForBubbles();
&& mTaskbarVisible;
}
private float computeScrimAlpha() {
boolean isTransient = mActivity.isTransientTaskbar();
final boolean isPersistentTaskBarVisible = mTaskbarVisible && !isTransient;
private float getScrimAlpha() {
final boolean isPersistentTaskBarVisible =
mTaskbarVisible && !DisplayController.isTransientTaskbar(mScrimView.getContext());
final boolean manageMenuExpanded =
(mSysUiStateFlags & SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0;
if (isPersistentTaskBarVisible && manageMenuExpanded) {
@@ -142,7 +123,7 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa
mScrimView.setOnClickListener(showScrim ? (view) -> onClick() : null);
mScrimView.setClickable(showScrim);
if (skipAnim) {
mScrimAlpha.updateValue(alpha);
mScrimView.setScrimAlpha(alpha);
} else {
ObjectAnimator anim = mScrimAlpha.animateToValue(showScrim ? alpha : 0);
anim.setInterpolator(showScrim ? SCRIM_ALPHA_IN : SCRIM_ALPHA_OUT);
@@ -155,7 +136,7 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa
}
private void onClick() {
SystemUiProxy.INSTANCE.get(mActivity).onBackEvent(null);
SystemUiProxy.INSTANCE.get(mActivity).onBackPressed();
}
@Override
@@ -169,14 +150,4 @@ public class TaskbarScrimViewController implements TaskbarControllers.LoggableTa
pw.println(prefix + "\tmScrimAlpha.value=" + mScrimAlpha.value);
}
@VisibleForTesting
TaskbarScrimView getScrimView() {
return mScrimView;
}
@VisibleForTesting
float getScrimAlpha() {
return mScrimAlpha.value;
}
}
@@ -30,10 +30,6 @@ import android.os.IBinder;
import android.view.InsetsFrameProvider;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.bubbles.BubbleInfo;
import java.util.List;
/**
* State shared across different taskbar instance
@@ -60,30 +56,14 @@ public class TaskbarSharedState {
// TaskbarManager#onNavButtonsDarkIntensityChanged()
public float navButtonsDarkIntensity;
// TaskbarManager#onTransitionModeUpdated()
public int barMode;
// TaskbarManager#onNavigationBarLumaSamplingEnabled()
public int mLumaSamplingDisplayId = DEFAULT_DISPLAY;
public boolean mIsLumaSamplingEnabled = true;
public boolean setupUIVisible = false;
public boolean wallpaperVisible = false;
public boolean allAppsVisible = false;
public BubbleBarLocation bubbleBarLocation;
public List<BubbleInfo> bubbleInfoItems;
public List<BubbleInfo> suppressedBubbleInfoItems;
/** Returns whether there are a saved bubbles. */
public boolean hasSavedBubbles() {
return bubbleInfoItems != null && !bubbleInfoItems.isEmpty();
}
// LauncherTaskbarUIController#mTaskbarInAppDisplayProgressMultiProp
public float[] inAppDisplayProgressMultiPropValues = new float[DISPLAY_PROGRESS_COUNT];
@@ -117,8 +97,5 @@ public class TaskbarSharedState {
// To track if taskbar was stashed / unstashed between configuration changes (which recreates
// the task bar).
public boolean taskbarWasStashedAuto = true;
// should show corner radius on persistent taskbar when in desktop mode.
public boolean showCornerRadiusInDesktopMode = false;
public Boolean taskbarWasStashedAuto = true;
}
@@ -22,7 +22,6 @@ import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventFo
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.View;
@@ -39,7 +38,6 @@ import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.ShortcutUtil;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.LogUtils;
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import java.util.List;
@@ -52,7 +50,6 @@ public class TaskbarShortcutMenuAccessibilityDelegate
public static final int MOVE_TO_TOP_OR_LEFT = R.id.action_move_to_top_or_left;
public static final int MOVE_TO_BOTTOM_OR_RIGHT = R.id.action_move_to_bottom_or_right;
public static final int CREATE_APPLICATION_BUBBLE = R.id.action_create_application_bubble;
private final LauncherApps mLauncherApps;
private final StatsLogManager mStatsLogManager;
@@ -70,9 +67,6 @@ public class TaskbarShortcutMenuAccessibilityDelegate
MOVE_TO_BOTTOM_OR_RIGHT,
R.string.move_drop_target_bottom_or_right,
KeyEvent.KEYCODE_R));
mActions.put(CREATE_APPLICATION_BUBBLE, new LauncherAction(
CREATE_APPLICATION_BUBBLE, R.string.open_app_as_a_bubble,
KeyEvent.KEYCODE_L));
}
@Override
@@ -82,27 +76,11 @@ public class TaskbarShortcutMenuAccessibilityDelegate
}
out.add(mActions.get(MOVE_TO_TOP_OR_LEFT));
out.add(mActions.get(MOVE_TO_BOTTOM_OR_RIGHT));
if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
out.add(mActions.get(CREATE_APPLICATION_BUBBLE));
}
}
@Override
protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) {
if (action == DEEP_SHORTCUTS) {
mContext.showPopupMenuForIcon((BubbleTextView) host);
return true;
} else if (action == CREATE_APPLICATION_BUBBLE) {
if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
&& item instanceof WorkspaceItemInfo) {
ShortcutInfo shortcutInfo = ((WorkspaceItemInfo) item).getDeepShortcutInfo();
SystemUiProxy.INSTANCE.get(mContext).showShortcutBubble(shortcutInfo);
return true;
} else if (item.getIntent() != null && item.getIntent().getPackage() != null) {
SystemUiProxy.INSTANCE.get(mContext).showAppBubble(item.getIntent(), item.user);
return true;
}
} else if (item instanceof ItemInfoWithIcon
if (item instanceof ItemInfoWithIcon
&& (action == MOVE_TO_TOP_OR_LEFT || action == MOVE_TO_BOTTOM_OR_RIGHT)) {
ItemInfoWithIcon info = (ItemInfoWithIcon) item;
int side = action == MOVE_TO_TOP_OR_LEFT
@@ -133,6 +111,10 @@ public class TaskbarShortcutMenuAccessibilityDelegate
item.user.getIdentifier(), new Intent(), side, null,
instanceIds.first);
}
return true;
} else if (action == DEEP_SHORTCUTS) {
mContext.showPopupMenuForIcon((BubbleTextView) host);
return true;
}
return false;
@@ -27,6 +27,7 @@ import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.SpringAnimationBuilder;
import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController;
import com.android.launcher3.util.DisplayController;
import java.io.PrintWriter;
@@ -46,7 +47,7 @@ public class TaskbarSpringOnStashController implements LoggableTaskbarController
public TaskbarSpringOnStashController(TaskbarActivityContext context) {
mContext = context;
mIsTransientTaskbar = context.isTransientTaskbar();
mIsTransientTaskbar = DisplayController.isTransientTaskbar(mContext);
mStartVelocityPxPerS = context.getResources()
.getDimension(R.dimen.transient_taskbar_stash_spring_velocity_dp_per_s);
}
@@ -23,20 +23,16 @@ import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.internal.jank.InteractionJankMonitor.Configuration;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.Flags.syncAppLaunchWithTaskbarStash;
import static com.android.launcher3.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW;
import static com.android.launcher3.taskbar.TaskbarActivityContext.ENABLE_TASKBAR_BEHIND_SHADE;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
import static com.android.quickstep.util.SystemUiFlagUtils.isTaskbarHidden;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
@@ -64,9 +60,11 @@ import com.android.launcher3.Alarm;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.SystemUiFlagUtils;
@@ -84,11 +82,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
private static final String TAG = "TaskbarStashController";
private static final boolean DEBUG = false;
/**
* Def. value for @param shouldBubblesFollow in
* {@link #updateAndAnimateTransientTaskbar(boolean)} */
public static boolean SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE = true;
public static final int FLAG_IN_APP = 1 << 0;
public static final int FLAG_STASHED_IN_APP_SYSUI = 1 << 1; // shade open, ...
public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 2; // setup wizard and AllSetActivity
@@ -101,14 +94,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
public static final int FLAG_STASHED_SYSUI = 1 << 9; // app pinning,...
public static final int FLAG_STASHED_DEVICE_LOCKED = 1 << 10; // device is locked: keyguard, ...
public static final int FLAG_IN_OVERVIEW = 1 << 11; // launcher is in overview
// An internal no-op flag to determine whether we should delay the taskbar background animation
private static final int FLAG_DELAY_TASKBAR_BG_TAG = 1 << 12;
public static final int FLAG_STASHED_FOR_BUBBLES = 1 << 13; // show handle for stashed hotseat
public static final int FLAG_TASKBAR_HIDDEN = 1 << 14; // taskbar hidden during dream, etc...
// taskbar should always be stashed for bubble bar on phone
public static final int FLAG_STASHED_BUBBLE_BAR_ON_PHONE = 1 << 15;
public static final int FLAG_IGNORE_IN_APP = 1 << 16; // used to sync with app launch animation
// If any of these flags are enabled, isInApp should return true.
private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
@@ -130,30 +115,26 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
// If any of these flags are enabled, the taskbar must be stashed.
private static final int FLAGS_FORCE_STASHED = FLAG_STASHED_SYSUI | FLAG_STASHED_DEVICE_LOCKED
| FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN
| FLAG_STASHED_FOR_BUBBLES | FLAG_STASHED_BUBBLE_BAR_ON_PHONE;
| FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN;
/**
* How long to stash/unstash when manually invoked via long press.
*
* Use {@link #getStashDuration()} to query duration
*/
@VisibleForTesting
static final long TASKBAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE;
private static final long TASKBAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE;
/**
* How long to stash/unstash transient taskbar.
*
* Use {@link #getStashDuration()} to query duration.
*/
@VisibleForTesting
static final long TRANSIENT_TASKBAR_STASH_DURATION = 417;
private static final long TRANSIENT_TASKBAR_STASH_DURATION = 417;
/**
* How long to stash/unstash when keyboard is appearing/disappearing.
*/
@VisibleForTesting
static final long TASKBAR_STASH_DURATION_FOR_IME = 80;
private static final long TASKBAR_STASH_DURATION_FOR_IME = 80;
/**
* The scale TaskbarView animates to when being stashed.
@@ -169,12 +150,12 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
/**
* How long to delay the icon/stash handle alpha.
*/
public static final long TASKBAR_STASH_ALPHA_START_DELAY = 33;
private static final long TASKBAR_STASH_ALPHA_START_DELAY = 33;
/**
* How long the icon/stash handle alpha animation plays.
*/
public static final long TRANSIENT_TASKBAR_STASH_ALPHA_DURATION = 50;
private static final long TASKBAR_STASH_ALPHA_DURATION = 50;
/**
* How long to delay the icon/stash handle alpha for the home to app taskbar animation.
@@ -196,7 +177,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
// Duration for which an unlock event is considered "current", as other events are received
// asynchronously.
public static final long UNLOCK_TRANSITION_MEMOIZATION_MS = 200;
private static final long UNLOCK_TRANSITION_MEMOIZATION_MS = 200;
/**
* The default stash animation, morphing the taskbar into the navbar.
@@ -220,13 +201,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
*/
private static final int TRANSITION_UNSTASH_SUW_MANUAL = 3;
/**
* total duration of entering dream state animation, which we use as start delay to
* applyState() when SYSUI_STATE_DEVICE_DREAMING flag is present. Keep this in sync with
* DreamAnimationController.TOTAL_ANIM_DURATION.
*/
private static final int SKIP_TOTAL_DREAM_ANIM_DURATION = 450;
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
TRANSITION_DEFAULT,
@@ -263,17 +237,15 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
private @Nullable AnimatorSet mAnimator;
private boolean mIsSystemGestureInProgress;
/** Whether the IME is visible. */
private boolean mIsImeVisible;
private boolean mIsImeShowing;
private boolean mIsImeSwitcherShowing;
private final Alarm mTimeoutAlarm = new Alarm();
private boolean mEnableBlockingTimeoutDuringTests = false;
private Animator mTaskbarBackgroundAlphaAnimator;
private final long mTaskbarBackgroundDuration;
private boolean mUserIsNotGoingHome = false;
private final boolean mInAppStateAffectsDesktopTasksVisibilityInTaskbar;
private long mTaskbarBackgroundDuration;
private boolean mIsGoingHome;
// Evaluate whether the handle should be stashed
private final LongPredicate mIsStashedPredicate = flags -> {
@@ -299,18 +271,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity);
mAccessibilityManager = mActivity.getSystemService(AccessibilityManager.class);
// Taskbar, via `TaskbarDesktopModeController`, depends on `TaskbarStashController` state to
// determine whether desktop tasks should be shown because taskbar is pinned on the home
// screen for freeform windowing displays. In this case, list of items shown in the taskbar
// needs to be updated when in-app state changes.
// TODO(b/390665752): Feature to "lock" pinned taskbar to home screen will be superseded by
// pinning, in other launcher states, at which point this variable can be removed.
mInAppStateAffectsDesktopTasksVisibilityInTaskbar =
!mActivity.showDesktopTaskbarForFreeformDisplay()
&& mActivity.showLockedTaskbarOnHome();
mTaskbarBackgroundDuration = activity.getResources().getInteger(
R.integer.taskbar_background_duration);
mTaskbarBackgroundDuration =
activity.getResources().getInteger(R.integer.taskbar_background_duration);
if (mActivity.isPhoneMode()) {
mUnstashedHeight = mActivity.getResources().getDimensionPixelSize(
R.dimen.taskbar_phone_size);
@@ -322,6 +284,18 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
}
}
/**
* Show Taskbar upon receiving broadcast
*/
public void showTaskbarFromBroadcast() {
// If user is in middle of taskbar education handle go to next step of education
if (mControllers.taskbarEduTooltipController.isBeforeTooltipFeaturesStep()) {
mControllers.taskbarEduTooltipController.hide();
mControllers.taskbarEduTooltipController.maybeShowFeaturesEdu();
}
updateAndAnimateTransientTaskbar(false);
}
/**
* Initializes the controller
*/
@@ -349,7 +323,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
StashedHandleViewController.ALPHA_INDEX_STASHED);
mTaskbarStashedHandleHintScale = stashedHandleController.getStashedHandleHintScale();
boolean isTransientTaskbar = mActivity.isTransientTaskbar();
boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
boolean isInSetup = !mActivity.isUserSetupComplete() || setupUIVisible;
boolean isStashedInAppAuto =
isTransientTaskbar && !mTaskbarSharedState.getTaskbarWasPinned();
@@ -364,16 +338,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
// For now, assume we're in an app, since LauncherTaskbarUIController won't be able to tell
// us that we're paused until a bit later. This avoids flickering upon recreating taskbar.
updateStateForFlag(FLAG_IN_APP, true);
updateStateForFlag(FLAG_STASHED_BUBBLE_BAR_ON_PHONE, mActivity.isBubbleBarOnPhone());
applyState(/* duration = */ 0);
// Hide the background while stashed so it doesn't show on fast swipes home
boolean shouldHideTaskbarBackground = mActivity.isPhoneMode() ||
(enableScalingRevealHomeAnimation() && isTransientTaskbar && isStashed());
mTaskbarBackgroundAlphaForStash.setValue(shouldHideTaskbarBackground ? 0 : 1);
if (mTaskbarSharedState.getTaskbarWasPinned()
|| !mTaskbarSharedState.taskbarWasStashedAuto) {
tryStartTaskbarTimeout();
@@ -412,10 +377,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
* Returns how long the stash/unstash animation should play.
*/
public long getStashDuration() {
if (mActivity.isPinnedTaskbar()) {
return PINNED_TASKBAR_TRANSITION_DURATION;
}
return mActivity.isTransientTaskbar() ? TRANSIENT_TASKBAR_STASH_DURATION
return DisplayController.isTransientTaskbar(mActivity)
? TRANSIENT_TASKBAR_STASH_DURATION
: TASKBAR_STASH_DURATION;
}
@@ -426,28 +389,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
return mIsStashed;
}
public boolean isDeviceLocked() {
return hasAnyFlag(FLAG_STASHED_DEVICE_LOCKED);
}
/**
* Sets the hotseat stashed.
* b/373429249 - we might change this behavior if we remove the scrim, that's why we're keeping
* this method
*/
public void stashHotseat(boolean stash) {
mControllers.uiController.stashHotseat(stash);
}
/**
* Instantly un-stashes the hotseat.
* * b/373429249 - we might change this behavior if we remove the scrim, that's why we're
* keeping this method
*/
public void unStashHotseatInstantly() {
mControllers.uiController.unStashHotseatInstantly();
}
/**
* Returns whether the taskbar should be stashed in apps (e.g. user long pressed to stash).
*/
@@ -482,21 +423,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
return hasAnyFlag(FLAGS_IN_APP);
}
/** Returns whether the taskbar is currently in overview screen. */
public boolean isInOverview() {
return hasAnyFlag(FLAG_IN_OVERVIEW);
}
/** Returns whether the taskbar is currently on launcher home screen. */
public boolean isOnHome() {
return !isInOverview() && !isInApp();
}
/** Returns whether taskbar is hidden for bubbles. */
public boolean isHiddenForBubbles() {
return hasAnyFlag(FLAG_STASHED_FOR_BUBBLES);
}
/**
* Returns the height that taskbar will be touchable.
*/
@@ -513,8 +439,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
* @see android.view.WindowInsets.Type#systemBars()
*/
public int getContentHeightToReportToApps() {
boolean isTransient = mActivity.isTransientTaskbar();
if (mActivity.isUserSetupComplete() && (mActivity.isPhoneGestureNavMode() || isTransient)) {
if (mActivity.isUserSetupComplete() && (mActivity.isPhoneGestureNavMode()
|| DisplayController.isTransientTaskbar(mActivity))) {
return getStashedHeight();
}
@@ -559,17 +485,9 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
/**
* Stash or unstashes the transient taskbar, using the default TASKBAR_STASH_DURATION.
* If bubble bar exists, it will match taskbars stashing behavior.
* Will not delay taskbar background by default.
*/
public void updateAndAnimateTransientTaskbar(boolean stash) {
updateAndAnimateTransientTaskbar(stash, SHOULD_BUBBLES_FOLLOW_DEFAULT_VALUE, false);
}
/**
* Stash or unstashes the transient taskbar, using the default TASKBAR_STASH_DURATION.
*/
public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow) {
updateAndAnimateTransientTaskbar(stash, shouldBubblesFollow, false);
updateAndAnimateTransientTaskbar(stash, /* shouldBubblesFollow= */ true);
}
/**
@@ -577,47 +495,28 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
*
* @param stash whether transient taskbar should be stashed.
* @param shouldBubblesFollow whether bubbles should match taskbars behavior.
* @param delayTaskbarBackground whether we will delay the taskbar background animation
*/
public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow,
boolean delayTaskbarBackground) {
if (!mActivity.isTransientTaskbar() || mActivity.isBubbleBarOnPhone()) {
public void updateAndAnimateTransientTaskbar(boolean stash, boolean shouldBubblesFollow) {
if (!DisplayController.isTransientTaskbar(mActivity)) {
return;
}
if (stash
&& !mControllers.taskbarAutohideSuspendController
.isSuspendedForTransientTaskbarInLauncher()
&& mControllers.taskbarAutohideSuspendController
.isTransientTaskbarStashingSuspended()) {
if (
stash
&& !mControllers.taskbarAutohideSuspendController
.isSuspendedForTransientTaskbarInLauncher()
&& mControllers.taskbarAutohideSuspendController
.isTransientTaskbarStashingSuspended()) {
// Avoid stashing if autohide is currently suspended.
return;
}
boolean shouldApplyState = false;
if (delayTaskbarBackground) {
mControllers.taskbarStashController.updateStateForFlag(FLAG_DELAY_TASKBAR_BG_TAG, true);
shouldApplyState = true;
}
if (hasAnyFlag(FLAG_STASHED_IN_APP_AUTO) != stash) {
mTaskbarSharedState.taskbarWasStashedAuto = stash;
updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, stash);
shouldApplyState = true;
}
if (shouldApplyState) {
applyState();
}
// Effectively a no-opp to remove the tag.
if (delayTaskbarBackground) {
mControllers.taskbarStashController.updateStateForFlag(FLAG_DELAY_TASKBAR_BG_TAG,
false);
mControllers.taskbarStashController.applyState(0);
}
mControllers.bubbleControllers.ifPresent(controllers -> {
if (shouldBubblesFollow) {
final boolean willStash = mIsStashedPredicate.test(mState);
@@ -649,7 +548,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
/** Toggles the Taskbar's stash state. */
public void toggleTaskbarStash() {
if (!mActivity.isTransientTaskbar() || !hasAnyFlag(FLAGS_IN_APP)) return;
if (!DisplayController.isTransientTaskbar(mActivity) || !hasAnyFlag(FLAGS_IN_APP)) return;
updateAndAnimateTransientTaskbar(!hasAnyFlag(FLAG_STASHED_IN_APP_AUTO));
}
@@ -671,7 +570,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
/* isStashed= */ mActivity.isPhoneMode(),
placeholderDuration,
TRANSITION_UNSTASH_SUW_MANUAL,
/* skipTaskbarBackgroundDelay */ false,
/* jankTag= */ "SUW_MANUAL");
animation.addListener(AnimatorListeners.forEndCallback(
() -> mControllers.taskbarViewController.setDeferUpdatesForSUW(false)));
@@ -681,14 +579,13 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
/**
* Create a stash animation and save to {@link #mAnimator}.
*
* @param isStashed whether it's a stash animation or an unstash animation
* @param duration duration of the animation
* @param animationType what transition type to play.
* @param shouldDelayBackground whether we should delay the taskbar bg animation
* @param jankTag tag to be used in jank monitor trace.
* @param isStashed whether it's a stash animation or an unstash animation
* @param duration duration of the animation
* @param animationType what transition type to play.
* @param jankTag tag to be used in jank monitor trace.
*/
private void createAnimToIsStashed(boolean isStashed, long duration,
@StashAnimation int animationType, boolean shouldDelayBackground, String jankTag) {
@StashAnimation int animationType, String jankTag) {
if (animationType == TRANSITION_UNSTASH_SUW_MANUAL && isStashed) {
// The STASH_ANIMATION_SUW_MANUAL must only be used during an unstash animation.
Log.e(TAG, "Illegal arguments:Using TRANSITION_UNSTASH_SUW_MANUAL to stash taskbar");
@@ -700,7 +597,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
mAnimator = new AnimatorSet();
addJankMonitorListener(
mAnimator, /* expanding= */ !isStashed, /* tag= */ jankTag);
final float stashTranslation = mActivity.isPhoneMode() || mActivity.isTransientTaskbar()
boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
final float stashTranslation = mActivity.isPhoneMode() || isTransientTaskbar
? 0
: (mUnstashedHeight - mStashedHeight);
@@ -724,9 +622,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
return;
}
if (mActivity.isTransientTaskbar()) {
createTransientAnimToIsStashed(mAnimator, isStashed, duration,
shouldDelayBackground, animationType);
if (isTransientTaskbar) {
createTransientAnimToIsStashed(mAnimator, isStashed, duration, animationType);
} else {
createAnimToIsStashed(mAnimator, isStashed, duration, stashTranslation, animationType);
}
@@ -832,7 +729,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
}
private void createTransientAnimToIsStashed(AnimatorSet as, boolean isStashed, long duration,
boolean shouldDelayBackground, @StashAnimation int animationType) {
@StashAnimation int animationType) {
// Target values of the properties this is going to set
final float backgroundOffsetTarget = isStashed ? 1 : 0;
final float iconAlphaTarget = isStashed ? 0 : 1;
@@ -846,14 +743,14 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
if (animationType == TRANSITION_HANDLE_FADE) {
// When fading, the handle fades in/out at the beginning of the transition with
// TASKBAR_STASH_ALPHA_DURATION.
backgroundAndHandleAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION;
backgroundAndHandleAlphaDuration = TASKBAR_STASH_ALPHA_DURATION;
// The iconAlphaDuration must be set to duration for the skippable interpolators
// below to work.
iconAlphaDuration = duration;
} else {
iconAlphaStartDelay = TASKBAR_STASH_ALPHA_START_DELAY;
iconAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION;
backgroundAndHandleAlphaDuration = TRANSIENT_TASKBAR_STASH_ALPHA_DURATION;
iconAlphaDuration = TASKBAR_STASH_ALPHA_DURATION;
backgroundAndHandleAlphaDuration = TASKBAR_STASH_ALPHA_DURATION;
if (isStashed) {
if (animationType == TRANSITION_HOME_TO_APP) {
@@ -870,10 +767,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
backgroundAndHandleAlphaStartDelay,
backgroundAndHandleAlphaDuration, LINEAR);
if (enableScalingRevealHomeAnimation()
&& !isStashed
&& shouldDelayBackground) {
if (enableScalingRevealHomeAnimation() && !isStashed) {
play(as, getTaskbarBackgroundAnimatorWhenNotGoingHome(duration),
0, 0, LINEAR);
as.addListener(AnimatorListeners.forEndCallback(
@@ -934,13 +828,17 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
private boolean mTaskbarBgAlphaAnimationStarted = false;
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
if (mIsGoingHome) {
mTaskbarBgAlphaAnimationStarted = true;
}
if (mTaskbarBgAlphaAnimationStarted) {
return;
}
if (valueAnimator.getAnimatedFraction() >= ANIMATED_FRACTION_THRESHOLD) {
if (mUserIsNotGoingHome) {
if (!mIsGoingHome) {
playTaskbarBackgroundAlphaAnimation();
setUserIsGoingHome(false);
mTaskbarBgAlphaAnimationStarted = true;
}
}
@@ -952,8 +850,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
/**
* Sets whether the user is going home based on the current gesture.
*/
public void setUserIsNotGoingHome(boolean userIsNotGoingHome) {
mUserIsNotGoingHome = userIsNotGoingHome;
public void setUserIsGoingHome(boolean isGoingHome) {
mIsGoingHome = isGoingHome;
}
/**
@@ -998,7 +896,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
}
int action = expanding ? InteractionJankMonitor.CUJ_TASKBAR_EXPAND :
InteractionJankMonitor.CUJ_TASKBAR_COLLAPSE;
animator.addListener(new AnimationSuccessListener() {
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(@NonNull Animator animation) {
final Configuration.Builder builder =
@@ -1010,16 +908,9 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
}
@Override
public void onAnimationSuccess(@NonNull Animator animator) {
public void onAnimationEnd(@NonNull Animator animation) {
InteractionJankMonitor.getInstance().end(action);
}
@Override
public void onAnimationCancel(@NonNull Animator animation) {
super.onAnimationCancel(animation);
InteractionJankMonitor.getInstance().cancel(action);
}
});
}
@@ -1049,29 +940,13 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
}
public void applyState() {
applyState(/* postApplyAction = */ null);
}
/** Applies state and performs action after state is applied. */
public void applyState(@Nullable Runnable postApplyAction) {
applyState(hasAnyFlag(FLAG_IN_SETUP) ? 0 : TASKBAR_STASH_DURATION, postApplyAction);
applyState(hasAnyFlag(FLAG_IN_SETUP) ? 0 : TASKBAR_STASH_DURATION);
}
public void applyState(long duration) {
applyState(duration, /* postApplyAction = */ null);
}
private void applyState(long duration, @Nullable Runnable postApplyAction) {
Animator animator = createApplyStateAnimator(duration);
if (animator != null) {
if (postApplyAction != null) {
// performs action on animation end
animator.addListener(AnimatorListeners.forEndCallback(postApplyAction));
}
animator.start();
} else if (postApplyAction != null) {
// animator was not created, just execute the action
postApplyAction.run();
}
}
@@ -1089,9 +964,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
*/
@Nullable
public Animator createApplyStateAnimator(long duration) {
if (mActivity.isPhoneMode()) {
return null;
}
return mStatePropertyHolder.createSetStateAnimator(mState, duration);
}
@@ -1124,9 +996,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
/**
* When hiding the IME, delay the unstash animation to align with the end of the transition.
*/
@VisibleForTesting
long getTaskbarStashStartDelayForIme() {
if (mIsImeVisible) {
private long getTaskbarStashStartDelayForIme() {
if (mIsImeShowing) {
// Only delay when IME is exiting, not entering.
return 0;
}
@@ -1142,36 +1013,28 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
long startDelay = 0;
updateStateForFlag(FLAG_STASHED_IN_APP_SYSUI, hasAnyFlag(systemUiStateFlags,
SYSUI_STATE_DIALOG_SHOWING | (ENABLE_TASKBAR_BEHIND_SHADE.isTrue()
? 0
: SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE)
));
SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE));
boolean stashForBubbles = hasAnyFlag(FLAG_IN_OVERVIEW)
&& hasAnyFlag(systemUiStateFlags, SYSUI_STATE_BUBBLES_EXPANDED)
&& mActivity.isTransientTaskbar();
&& DisplayController.isTransientTaskbar(mActivity);
updateStateForFlag(FLAG_STASHED_SYSUI,
hasAnyFlag(systemUiStateFlags, SYSUI_STATE_SCREEN_PINNING) || stashForBubbles);
updateStateForFlag(FLAG_STASHED_DEVICE_LOCKED,
SystemUiFlagUtils.isLocked(systemUiStateFlags));
mIsImeVisible = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_VISIBLE);
mIsImeShowing = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SHOWING);
mIsImeSwitcherShowing = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SWITCHER_SHOWING);
if (updateStateForFlag(FLAG_STASHED_IME, shouldStashForIme())) {
animDuration = TASKBAR_STASH_DURATION_FOR_IME;
startDelay = getTaskbarStashStartDelayForIme();
}
if (isTaskbarHidden(systemUiStateFlags) && !hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags));
applyState(0, SKIP_TOTAL_DREAM_ANIM_DURATION);
} else {
updateStateForFlag(FLAG_TASKBAR_HIDDEN, isTaskbarHidden(systemUiStateFlags));
applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay);
}
applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay);
}
/**
* We stash when the IME is visible.
* We stash when IME or IME switcher is showing.
*
* <p>Do not stash if in small screen, with 3 button nav, and in landscape (or seascape).
* <p>Do not stash if taskbar is transient.
@@ -1179,7 +1042,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
* <p>Do not stash if a system gesture is started.
*/
private boolean shouldStashForIme() {
if (mActivity.isTransientTaskbar()) {
if (DisplayController.isTransientTaskbar(mActivity)) {
return false;
}
// Do not stash if in small screen, with 3 button nav, and in landscape.
@@ -1189,16 +1052,16 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
}
// Do not stash if pinned taskbar, hardware keyboard is attached and no IME is docked
if (mActivity.isHardwareKeyboard() && mActivity.isPinnedTaskbar()
if (mActivity.isHardwareKeyboard() && DisplayController.isPinnedTaskbar(mActivity)
&& !mActivity.isImeDocked()) {
return false;
}
// Do not stash if hardware keyboard is attached, in 3 button nav and desktop windowing mode
if (mActivity.isHardwareKeyboard()
&& mActivity.isThreeButtonNav()
&& mControllers.taskbarDesktopModeController
.isInDesktopModeAndNotInOverview(mActivity.getDisplayId())) {
DesktopVisibilityController visibilityController =
LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
if (visibilityController != null && mActivity.isHardwareKeyboard()
&& mActivity.isThreeButtonNav() && visibilityController.areDesktopTasksVisible()) {
return false;
}
@@ -1207,7 +1070,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
return false;
}
return mIsImeVisible;
return mIsImeShowing || mIsImeSwitcherShowing;
}
/**
@@ -1253,14 +1116,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TRANSIENT_TASKBAR,
!hasAnyFlag(FLAG_STASHED_IN_APP_AUTO));
}
if (hasAnyFlag(changedFlags, FLAG_IN_OVERVIEW | FLAG_IN_APP)) {
mControllers.runAfterInit(() -> mControllers.taskbarInsetsController
.onTaskbarOrBubblebarWindowHeightOrInsetsChanged());
if (mInAppStateAffectsDesktopTasksVisibilityInTaskbar) {
mControllers.runAfterInit(
() -> mControllers.taskbarViewController.commitRunningAppsToUI());
}
}
mActivity.applyForciblyShownFlagWhileTransientTaskbarUnstashed(!isStashedInApp());
}
@@ -1275,8 +1130,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
*/
public void setUpTaskbarSystemAction(boolean visible) {
UI_HELPER_EXECUTOR.execute(() -> {
if (!visible || !mActivity.isTransientTaskbar()
|| mActivity.isPhoneMode()) {
if (!visible || !DisplayController.isTransientTaskbar(mActivity)) {
mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR);
mIsTaskbarSystemActionRegistered = false;
return;
@@ -1300,12 +1154,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
* Clean up on destroy from TaskbarControllers
*/
public void onDestroy() {
// If the controller is destroyed before the animation finishes, we cancel the animation
// so that we don't finish the CUJ.
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
UI_HELPER_EXECUTOR.execute(
() -> mAccessibilityManager.unregisterSystemAction(SYSTEM_ACTION_ID_TASKBAR));
}
@@ -1326,7 +1174,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
* If false, attempts to re/start the timeout
*/
public void updateTaskbarTimeout(boolean isAutohideSuspended) {
if (!mActivity.isTransientTaskbar()) {
if (!DisplayController.isTransientTaskbar(mActivity)) {
return;
}
if (isAutohideSuspended) {
@@ -1339,8 +1187,10 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
/**
* Attempts to start timer to auto hide the taskbar based on time.
*/
private void tryStartTaskbarTimeout() {
if (!mActivity.isTransientTaskbar() || mIsStashed || mEnableBlockingTimeoutDuringTests) {
public void tryStartTaskbarTimeout() {
if (!DisplayController.isTransientTaskbar(mActivity)
|| mIsStashed
|| mEnableBlockingTimeoutDuringTests) {
return;
}
@@ -1365,11 +1215,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
updateAndAnimateTransientTaskbarForTimeout();
}
@VisibleForTesting
Alarm getTimeoutAlarm() {
return mTimeoutAlarm;
}
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "TaskbarStashController:");
@@ -1380,7 +1225,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
pw.println(prefix + "\tappliedState=" + getStateString(mStatePropertyHolder.mPrevFlags));
pw.println(prefix + "\tmState=" + getStateString(mState));
pw.println(prefix + "\tmIsSystemGestureInProgress=" + mIsSystemGestureInProgress);
pw.println(prefix + "\tmIsImeVisible=" + mIsImeVisible);
pw.println(prefix + "\tmIsImeShowing=" + mIsImeShowing);
pw.println(prefix + "\tmIsImeSwitcherShowing=" + mIsImeSwitcherShowing);
}
private static String getStateString(long flags) {
@@ -1422,13 +1268,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
*/
@Nullable
public Animator createSetStateAnimator(long flags, long duration) {
// We do this when we want to synchronize the app launch and taskbar stash animations.
if (syncAppLaunchWithTaskbarStash()
&& hasAnyFlag(FLAG_IGNORE_IN_APP)
&& hasAnyFlag(flags, FLAG_IN_APP)) {
flags = flags & ~FLAG_IN_APP;
}
boolean isStashed = mStashCondition.test(flags);
if (DEBUG) {
@@ -1478,9 +1317,8 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
mIsStashed = isStashed;
mLastStartedTransitionType = animationType;
boolean shouldDelayBackground = hasAnyFlag(FLAG_DELAY_TASKBAR_BG_TAG);
// This sets mAnimator.
createAnimToIsStashed(mIsStashed, duration, animationType, shouldDelayBackground,
createAnimToIsStashed(mIsStashed, duration, animationType,
computeTaskbarJankMonitorTag(changedFlags));
return mAnimator;
}
@@ -23,6 +23,7 @@ import com.android.launcher3.testing.shared.ResourceUtils
import com.android.launcher3.touch.SingleAxisSwipeDetector
import com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE
import com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.TouchController
import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer
@@ -38,7 +39,7 @@ import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer
class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : TouchController {
private val activity: TaskbarActivityContext = controllers.taskbarActivityContext
private val enabled = activity.isTransientTaskbar
private val enabled = DisplayController.isTransientTaskbar(activity)
private val swipeDownDetector: SingleAxisSwipeDetector
private val translationCallback = controllers.taskbarTranslationController.transitionCallback
/** Interpolator to apply resistance as user swipes down to the bottom of the screen. */
@@ -66,7 +67,7 @@ class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : Touc
val gestureHeight: Int =
ResourceUtils.getNavbarSize(
ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE,
activity.resources,
activity.resources
)
gestureHeightYThreshold = (activity.deviceProfile.heightPx - gestureHeight).toFloat()
}
@@ -88,7 +89,7 @@ class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : Touc
maxTouchDisplacement,
0f,
maxVisualDisplacement,
displacementInterpolator,
displacementInterpolator
)
)
return false
@@ -25,6 +25,7 @@ import androidx.core.content.res.ResourcesCompat;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.config.FeatureFlags;
/**
* Utility class that contains the different taskbar thresholds logic.
@@ -38,6 +39,10 @@ public class TaskbarThresholdUtils {
private static int getThreshold(Resources r, DeviceProfile dp, int thresholdDimen,
int multiplierDimen) {
if (!FeatureFlags.ENABLE_DYNAMIC_TASKBAR_THRESHOLDS.get()) {
return r.getDimensionPixelSize(thresholdDimen);
}
float landscapeScreenHeight = dp.isLandscape ? dp.heightPx : dp.widthPx;
float screenPart = (landscapeScreenHeight * SCREEN_UNITS);
float defaultDp = dpiFromPx(screenPart, DisplayMetrics.DENSITY_DEVICE_STABLE);
@@ -29,6 +29,7 @@ import androidx.dynamicanimation.animation.SpringForce;
import com.android.app.animation.Interpolators;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.SpringAnimationBuilder;
import com.android.launcher3.util.DisplayController;
import java.io.PrintWriter;
@@ -62,7 +63,7 @@ public class TaskbarTranslationController implements TaskbarControllers.Loggable
public TaskbarTranslationController(TaskbarActivityContext context) {
mContext = context;
mIsTransientTaskbar = mContext.isTransientTaskbar();
mIsTransientTaskbar = DisplayController.isTransientTaskbar(mContext);
mCallback = new TransitionCallback();
}
@@ -94,8 +95,7 @@ public class TaskbarTranslationController implements TaskbarControllers.Loggable
mControllers.taskbarDragLayerController.setTranslationYForSwipe(transY);
mControllers.bubbleControllers.ifPresent(controllers -> {
controllers.bubbleBarViewController.setTranslationYForSwipe(transY);
controllers.bubbleStashedHandleViewController.ifPresent(
controller -> controller.setTranslationYForSwipe(transY));
controllers.bubbleStashedHandleViewController.setTranslationYForSwipe(transY);
});
}
@@ -20,8 +20,8 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP;
import static com.android.quickstep.OverviewCommandHelper.TYPE_HIDE;
import android.animation.Animator;
import android.content.Intent;
import android.graphics.drawable.BitmapDrawable;
import android.view.MotionEvent;
@@ -37,17 +37,16 @@ import com.android.launcher3.Utilities;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.taskbar.bubbles.BubbleBarController;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.quickstep.GestureState;
import com.android.quickstep.RecentsAnimationCallbacks;
import com.android.quickstep.util.SplitTask;
import com.android.quickstep.OverviewCommandHelper;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.TISBindHelper;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
import com.android.quickstep.views.TaskView.TaskContainer;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import java.io.PrintWriter;
import java.util.Collections;
@@ -56,14 +55,12 @@ import java.util.stream.Stream;
/**
* Base class for providing different taskbar UI
*/
public class TaskbarUIController implements BubbleBarController.BubbleBarLocationListener {
public class TaskbarUIController {
public static final TaskbarUIController DEFAULT = new TaskbarUIController();
// Initialized in init.
protected TaskbarControllers mControllers;
protected boolean mSkipLauncherVisibilityChange;
@CallSuper
protected void init(TaskbarControllers taskbarControllers) {
mControllers = taskbarControllers;
@@ -79,27 +76,41 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
}
/**
* This should only be called by TaskbarStashController so that a TaskbarUIController can
* This should only be called by TaskbarStashController so that a
* TaskbarUIController can
* disable stashing. All other controllers should use
* {@link TaskbarStashController#supportsVisualStashing()} as the source of truth.
* {@link TaskbarStashController#supportsVisualStashing()} as the source of
* truth.
*/
public boolean supportsVisualStashing() {
return true;
}
protected void onStashedInAppChanged() { }
protected void onStashedInAppChanged() {
}
/**
* Called when taskbar icon layout bounds change.
*/
protected void onIconLayoutBoundsChanged() { }
protected void onIconLayoutBoundsChanged() {
}
protected String getTaskbarUIControllerName() {
return "TaskbarUIController";
}
/** Called when an icon is launched. */
public void onTaskbarIconLaunched(ItemInfo item) { }
@CallSuper
public void onTaskbarIconLaunched(ItemInfo item) {
// When launching from Taskbar, e.g. from Overview, set FLAG_IN_APP immediately
// instead of
// waiting for onPause, to reduce potential visual noise during the app open
// transition.
if (mControllers.taskbarStashController == null)
return;
mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true);
mControllers.taskbarStashController.applyState();
}
public View getRootView() {
return mControllers.taskbarActivityContext.getDragLayer();
@@ -107,7 +118,9 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
/**
* Called when swiping from the bottom nav region in fully gestural mode.
* @param inProgress True if the animation started, false if we just settled on an end target.
*
* @param inProgress True if the animation started, false if we just settled on
* an end target.
*/
public void setSystemGestureInProgress(boolean inProgress) {
mControllers.taskbarStashController.setSystemGestureInProgress(inProgress);
@@ -117,15 +130,15 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
* Manually closes the overlay window.
*/
public void hideOverlayWindow() {
mControllers.keyboardQuickSwitchController.closeQuickSwitchView();
boolean isTransientTaskbar = mControllers.taskbarActivityContext.isTransientTaskbar();
if (!isTransientTaskbar || mControllers.taskbarAllAppsController.isOpen()) {
if (!DisplayController.isTransientTaskbar(mControllers.taskbarActivityContext)
|| mControllers.taskbarAllAppsController.isOpen()) {
mControllers.taskbarOverlayController.hideWindow();
}
}
/**
* User expands PiP to full-screen (or split-screen) mode, try to hide the Taskbar.
* User expands PiP to full-screen (or split-screen) mode, try to hide the
* Taskbar.
*/
public void onExpandPip() {
if (mControllers != null) {
@@ -164,18 +177,21 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
/**
* @param ev MotionEvent in screen coordinates.
* @return Whether any Taskbar item could handle the given MotionEvent if given the chance.
*
* @return Whether any Taskbar item could handle the given MotionEvent if given
* the chance.
*/
public boolean isEventOverAnyTaskbarItem(MotionEvent ev) {
return mControllers.taskbarViewController.isEventOverAnyItem(ev)
|| mControllers.navbarButtonsViewController.isEventOverAnyItem(ev);
}
/** Checks if the given {@link MotionEvent} is over the bubble bar views. */
public boolean isEventOverBubbleBarViews(MotionEvent ev) {
/**
* Checks if the given {@link MotionEvent} is over the bubble bar stash handle.
*/
public boolean isEventOverBubbleBarStashHandle(MotionEvent ev) {
return mControllers.bubbleControllers.map(
bubbleControllers ->
bubbleControllers.bubbleStashController.isEventOverBubbleBarViews(ev))
bubbleControllers -> bubbleControllers.bubbleStashController.isEventOverStashHandle(ev))
.orElse(false);
}
@@ -187,39 +203,23 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
}
/**
* Returns true if hotseat icons are on top of view hierarchy when aligned in the current state.
* Returns true if hotseat icons are on top of view hierarchy when aligned in
* the current state.
*/
public boolean isHotseatIconOnTopWhenAligned() {
return true;
}
public boolean isAnimatingToHotseat() {
return false;
}
/**
* Skips to the end of the animation to Hotseat - should only be used if
* {@link #isAnimatingToHotseat()} returns true.
*/
public void endAnimationToHotseat() {}
/** Returns {@code true} if Taskbar is currently within overview. */
protected boolean isInOverviewUi() {
return false;
}
/**
* Toggles all apps UI. Default implementation opens Taskbar All Apps, but may be overridden to
* open different Alls Apps variant depending on the context.
* @param focusSearch indicates whether All Apps should be opened with search input focused.
* Returns {@code true} if Home All Apps available instead of Taskbar All Apps.
*/
protected void toggleAllApps(boolean focusSearch) {
if (focusSearch) {
mControllers.taskbarAllAppsController.toggleSearch();
} else {
mControllers.taskbarAllAppsController.toggle();
}
protected boolean canToggleHomeAllApps() {
return false;
}
@CallSuper
@@ -232,8 +232,10 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
/**
* Returns RecentsView. Overwritten in LauncherTaskbarUIController and
* FallbackTaskbarUIController with Launcher-specific implementations. Returns null for other
* UI controllers (like DesktopTaskbarUIController) that don't have a RecentsView.
* FallbackTaskbarUIController with Launcher-specific implementations. Returns
* null for other
* UI controllers (like DesktopTaskbarUIController) that don't have a
* RecentsView.
*/
public @Nullable RecentsView getRecentsView() {
return null;
@@ -246,36 +248,30 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
}
recentsView.getSplitSelectController().findLastActiveTasksAndRunCallback(
Collections.singletonList(splitSelectSource.getItemInfo().getComponentKey()),
Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
false /* findExactPairMatch */,
foundTasks -> {
@Nullable Task foundTask = foundTasks[0];
@Nullable
Task foundTask = foundTasks[0];
splitSelectSource.alreadyRunningTaskId = foundTask == null
? INVALID_TASK_ID
: foundTask.key.id;
splitSelectSource.animateCurrentTaskDismissal = foundTask != null;
recentsView.initiateSplitSelect(splitSelectSource);
}
);
});
}
/**
* Uses the clicked Taskbar icon to launch a second app for splitscreen.
*/
public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) {
// When launching from Taskbar, e.g. from Overview, set FLAG_IN_APP immediately
// to reduce potential visual noise during the app open transition.
if (mControllers.taskbarStashController != null) {
mControllers.taskbarStashController.updateStateForFlag(FLAG_IN_APP, true);
mControllers.taskbarStashController.applyState();
}
RecentsView recents = getRecentsView();
recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
Collections.singletonList(info.getComponentKey()),
false /* findExactPairMatch */,
foundTasks -> {
@Nullable Task foundTask = foundTasks[0];
@Nullable
Task foundTask = foundTasks[0];
if (foundTask != null) {
TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id);
// TODO (b/266482558): This additional null check is needed because there
@@ -286,14 +282,13 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
if (foundTaskView != null) {
// There is already a running app of this type, use that as second app.
// Get index of task (0 or 1), in case it's a GroupedTaskView
TaskContainer taskContainer =
foundTaskView.getTaskContainerById(foundTask.key.id);
TaskContainer taskContainer = foundTaskView.getTaskContainerById(foundTask.key.id);
recents.confirmSplitSelect(
foundTaskView,
foundTask,
taskContainer.getIconView().getDrawable(),
taskContainer.getSnapshotView(),
taskContainer.getThumbnail(),
taskContainer.getThumbnailViewDeprecated(),
taskContainer.getThumbnailViewDeprecated().getThumbnail(),
null /* intent */,
null /* user */,
info);
@@ -311,14 +306,14 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
intent,
info.user,
info);
}
);
});
}
/**
* Opens the Keyboard Quick Switch View.
*
* This will set the focus to the first task from the right (from the left in RTL)
* This will set the focus to the first task from the right (from the left in
* RTL)
*/
public void openQuickSwitchView() {
mControllers.keyboardQuickSwitchController.openQuickSwitchView();
@@ -327,10 +322,13 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
/**
* Launches the focused task and closes the Keyboard Quick Switch View.
*
* If the overlay or view are closed, or the overview task is focused, then Overview is
* launched. If the overview task is launched, then the first hidden task is focused.
* If the overlay or view are closed, or the overview task is focused, then
* Overview is
* launched. If the overview task is launched, then the first hidden task is
* focused.
*
* @return the index of what task should be focused in ; -1 iff Overview shouldn't be launched
* @return the index of what task should be focused in ; -1 iff Overview
* shouldn't be launched
*/
public int launchFocusedTask() {
int focusedTaskIndex = mControllers.keyboardQuickSwitchController.launchFocusedTask();
@@ -342,10 +340,12 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
* Launches the given task in split-screen.
*/
public void launchSplitTasks(
@NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) { }
@NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) {
}
/**
* Returns the matching view (if any) in the taskbar.
*
* @param view The view to match.
*/
public @Nullable View findMatchingView(View view) {
@@ -357,7 +357,8 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
return null;
}
// Taskbar has the same items as the hotseat and we can use screenId to find the match.
// Taskbar has the same items as the hotseat and we can use screenId to find the
// match.
int screenId = info.screenId;
View[] views = mControllers.taskbarViewController.getIconViews();
for (int i = views.length - 1; i >= 0; --i) {
@@ -371,7 +372,9 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
}
/**
* Callback for when launcher state transition completes after user swipes to home.
* Callback for when launcher state transition completes after user swipes to
* home.
*
* @param finalState The final state of the transition.
*/
public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) {
@@ -381,7 +384,8 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
/**
* Refreshes the resumed state of this ui controller.
*/
public void refreshResumedState() {}
public void refreshResumedState() {
}
/**
* Returns a stream of split screen menu options appropriate to the device.
@@ -394,19 +398,35 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
}
/** Adjusts the hotseat for the bubble bar. */
public void adjustHotseatForBubbleBar(boolean isBubbleBarVisible) {}
public void adjustHotseatForBubbleBar(boolean isBubbleBarVisible) {
}
@Nullable
protected TISBindHelper getTISBindHelper() {
return null;
}
/**
* Launches the focused task in the Keyboard Quick Switch view through the OverviewCommandHelper
* Launches the focused task in the Keyboard Quick Switch view through the
* OverviewCommandHelper
* <p>
* Use this helper method when the focused task may be the overview task.
*/
public void launchKeyboardFocusedTask() {
mControllers.navButtonController.hideOverview();
TISBindHelper tisBindHelper = getTISBindHelper();
if (tisBindHelper == null) {
return;
}
OverviewCommandHelper overviewCommandHelper = tisBindHelper.getOverviewCommandHelper();
if (overviewCommandHelper == null) {
return;
}
overviewCommandHelper.addCommand(TYPE_HIDE);
}
/**
* Adjusts the taskbar based on the visibility of the launcher.
*
* @param isVisible True if launcher is visible, false otherwise.
*/
public void onLauncherVisibilityChanged(boolean isVisible) {
@@ -415,7 +435,8 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
}
/**
* Request for UI controller to ignore animations for the next callback for the end of recents
* Request for UI controller to ignore animations for the next callback for the
* end of recents
* animation
*/
public void setSkipNextRecentsAnimEnd() {
@@ -425,51 +446,7 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio
/**
* Sets whether the user is going home based on the current gesture.
*/
public void setUserIsNotGoingHome(boolean isNotGoingHome) {
mControllers.taskbarStashController.setUserIsNotGoingHome(isNotGoingHome);
}
/**
* Sets whether to prevent taskbar from reacting to launcher visibility during the recents
* transition animation.
*/
public void setSkipLauncherVisibilityChange(boolean skip) {
mSkipLauncherVisibilityChange = skip;
}
/** Sets whether the hotseat is stashed */
public void stashHotseat(boolean stash) {
}
@Override
public void onBubbleBarLocationAnimated(BubbleBarLocation location) {
}
@Override
public void onBubbleBarLocationUpdated(BubbleBarLocation location) {
}
/** Un-stash the hotseat instantly */
public void unStashHotseatInstantly() {
}
/**
* Called when we want to unstash taskbar when user performs swipes up gesture.
*/
public void onSwipeToUnstashTaskbar() {
}
/**
* Called at the end of a gesture (see {@link GestureState.GestureEndTarget}).
* @param endTarget Where the gesture animation is going to.
* @param callbacks callbacks to track the recents animation lifecycle. The state change is
* automatically reset once the recents animation finishes
* @return An optional Animator to play in parallel with the default gesture end animation.
*/
public @Nullable Animator getParallelAnimationToGestureEndTarget(
GestureState.GestureEndTarget endTarget,
long duration,
RecentsAnimationCallbacks callbacks) {
return null;
public void setUserIsGoingHome(boolean isGoingHome) {
mControllers.taskbarStashController.setUserIsGoingHome(isGoingHome);
}
}
File diff suppressed because it is too large Load Diff
@@ -16,28 +16,15 @@
package com.android.launcher3.taskbar;
import static android.window.DesktopModeFlags.ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION;
import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP;
import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.jank.Cuj;
import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
/**
* Callbacks for {@link TaskbarView} to interact with its controller.
@@ -47,14 +34,12 @@ public class TaskbarViewCallbacks {
private final TaskbarActivityContext mActivity;
private final TaskbarControllers mControllers;
private final TaskbarView mTaskbarView;
private final GestureDetector mGestureDetector;
public TaskbarViewCallbacks(TaskbarActivityContext activity, TaskbarControllers controllers,
TaskbarView taskbarView) {
mActivity = activity;
mControllers = controllers;
mTaskbarView = taskbarView;
mGestureDetector = new GestureDetector(activity, new TaskbarViewGestureListener());
}
public View.OnClickListener getIconOnClickListener() {
@@ -62,40 +47,25 @@ public class TaskbarViewCallbacks {
}
/** Trigger All Apps button click action. */
public void triggerAllAppsButtonClick(View v) {
protected void triggerAllAppsButtonClick(View v) {
InteractionJankMonitorWrapper.begin(v, Cuj.CUJ_LAUNCHER_OPEN_ALL_APPS,
/* tag= */ "TASKBAR_BUTTON");
mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP);
if (mActivity.showLockedTaskbarOnHome()
|| mActivity.showDesktopTaskbarForFreeformDisplay()) {
// If the taskbar can be shown on the home screen, use mAllAppsToggler to toggle all
// apps, which will toggle the launcher activity all apps when on home screen.
// TODO(b/395913143): Reconsider this if a gap in taskbar all apps functionality that
// prevents users to drag items to workspace is addressed.
mControllers.uiController.toggleAllApps(false);
} else {
mControllers.taskbarAllAppsController.toggle();
}
mControllers.taskbarAllAppsController.toggle();
}
/** Trigger All Apps button long click action. */
public void triggerAllAppsButtonLongClick() {
protected void triggerAllAppsButtonLongClick() {
mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_LONG_PRESS);
}
/** @return true if haptic feedback should occur when long pressing the all apps button. */
public boolean isAllAppsButtonHapticFeedbackEnabled(Context context) {
public boolean isAllAppsButtonHapticFeedbackEnabled() {
return false;
}
@SuppressLint("ClickableViewAccessibility")
public View.OnTouchListener getTaskbarTouchListener() {
return (view, event) -> mGestureDetector.onTouchEvent(event);
}
public View.OnLongClickListener getTaskbarDividerLongClickListener() {
return v -> {
mControllers.taskbarPinningController.showPinningView(v, getDividerCenterX());
mControllers.taskbarPinningController.showPinningView(v);
return true;
};
}
@@ -103,9 +73,8 @@ public class TaskbarViewCallbacks {
public View.OnTouchListener getTaskbarDividerRightClickListener() {
return (v, event) -> {
if (event.isFromSource(InputDevice.SOURCE_MOUSE)
&& event.getAction() == MotionEvent.ACTION_DOWN
&& event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
mControllers.taskbarPinningController.showPinningView(v, getDividerCenterX());
mControllers.taskbarPinningController.showPinningView(v);
return true;
}
return false;
@@ -121,13 +90,6 @@ public class TaskbarViewCallbacks {
return new TaskbarHoverToolTipController(mActivity, mTaskbarView, icon);
}
/** Callback invoked before Taskbar icons are laid out. */
void onPreLayoutChildren() {
if (enableTaskbarPinning() && ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue()) {
mControllers.taskbarViewController.updateTaskbarIconTranslationXForPinning();
}
}
/**
* Notifies launcher to update icon alignment.
*/
@@ -142,116 +104,4 @@ public class TaskbarViewCallbacks {
mControllers.taskbarScrimViewController.onTaskbarVisibilityChanged(
mTaskbarView.getVisibility());
}
/**
* Get current location of bubble bar, if it is visible.
* Returns {@code null} if bubble bar is not shown.
*/
@Nullable
public BubbleBarLocation getBubbleBarLocationIfVisible() {
BubbleBarViewController bubbleBarViewController =
mControllers.bubbleControllers.map(c -> c.bubbleBarViewController).orElse(null);
if (bubbleBarViewController != null && bubbleBarViewController.isBubbleBarVisible()) {
return bubbleBarViewController.getBubbleBarLocation();
}
return null;
}
/**
* Get the max bubble bar collapsed width for the current bubble bar visibility state. Used to
* reserve space for the bubble bar when transitioning taskbar view into overflow.
*/
public float getBubbleBarMaxCollapsedWidthIfVisible() {
return mControllers.bubbleControllers
.filter(c -> !c.bubbleBarViewController.isHiddenForNoBubbles())
.map(c -> c.bubbleBarViewController.getCollapsedWidthWithMaxVisibleBubbles())
.orElse(0f);
}
/** Returns true if bubble bar controllers are present. */
public boolean isBubbleBarEnabled() {
return mControllers.bubbleControllers.isPresent();
}
/** Returns on click listener for the taskbar overflow view. */
public View.OnClickListener getOverflowOnClickListener() {
return new View.OnClickListener() {
@Override
public void onClick(View v) {
toggleKeyboardQuickSwitchView();
}
};
}
/** Returns on long click listener for the taskbar overflow view. */
public View.OnLongClickListener getOverflowOnLongClickListener() {
return new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
toggleKeyboardQuickSwitchView();
return true;
}
};
}
private void toggleKeyboardQuickSwitchView() {
if (mTaskbarView.getTaskbarOverflowView() != null) {
mTaskbarView.getTaskbarOverflowView().setIsActive(
!mTaskbarView.getTaskbarOverflowView().getIsActive());
mControllers.taskbarAutohideSuspendController
.updateFlag(FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW,
mTaskbarView.getTaskbarOverflowView().getIsActive());
}
mControllers.keyboardQuickSwitchController.toggleQuickSwitchViewForTaskbar(
mControllers.taskbarViewController.getTaskIdsForPinnedApps(),
this::onKeyboardQuickSwitchViewClosed);
}
private void onKeyboardQuickSwitchViewClosed() {
if (mTaskbarView.getTaskbarOverflowView() != null) {
mTaskbarView.getTaskbarOverflowView().setIsActive(false);
}
mControllers.taskbarAutohideSuspendController.updateFlag(
FLAG_AUTOHIDE_SUSPEND_TASKBAR_OVERFLOW, false);
}
private float getDividerCenterX() {
View divider = mTaskbarView.getTaskbarDividerViewContainer();
if (divider == null) {
return 0.0f;
}
return divider.getX() + (float) divider.getWidth() / 2;
}
private class TaskbarViewGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(@NonNull MotionEvent event) {
if (event.isFromSource(InputDevice.SOURCE_MOUSE)
&& event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
maybeShowPinningView(event);
}
return true;
}
@Override
public boolean onSingleTapUp(@NonNull MotionEvent event) {
return true;
}
@Override
public void onLongPress(@NonNull MotionEvent event) {
if (maybeShowPinningView(event)) {
mTaskbarView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
}
/** Returns true if the taskbar pinning popup view was shown for {@code event}. */
private boolean maybeShowPinningView(@NonNull MotionEvent event) {
if (!mActivity.isPinnedTaskbar() || mTaskbarView.isEventOverAnyItem(event)) {
return false;
}
mControllers.taskbarPinningController.showPinningView(mTaskbarView, event.getRawX());
return true;
}
}
}
@@ -16,14 +16,10 @@
package com.android.launcher3.taskbar
import android.app.contextualsearch.ContextualSearchManager.ENTRYPOINT_LONG_PRESS_META
import android.content.Context
import com.android.launcher3.R
import com.android.launcher3.logging.StatsLogManager
import com.android.launcher3.util.ResourceBasedOverride
import com.android.launcher3.util.ResourceBasedOverride.Overrides
import com.android.quickstep.TopTaskTracker
import com.android.quickstep.util.ContextualSearchInvoker
/** Creates [TaskbarViewCallbacks] instances. */
open class TaskbarViewCallbacksFactory : ResourceBasedOverride {
@@ -32,35 +28,7 @@ open class TaskbarViewCallbacksFactory : ResourceBasedOverride {
activity: TaskbarActivityContext,
controllers: TaskbarControllers,
taskbarView: TaskbarView,
): TaskbarViewCallbacks {
return object : TaskbarViewCallbacks(activity, controllers, taskbarView) {
override fun triggerAllAppsButtonLongClick() {
super.triggerAllAppsButtonLongClick()
val contextualSearchInvoked =
ContextualSearchInvoker(activity).show(ENTRYPOINT_LONG_PRESS_META)
if (contextualSearchInvoked) {
val runningPackage =
TopTaskTracker.INSTANCE[activity].getCachedTopTask(
/* filterOnlyVisibleRecents */ true,
activity.display.displayId,
)
.getPackageName()
activity.statsLogManager
.logger()
.withPackageName(runningPackage)
.log(StatsLogManager.LauncherEvent.LAUNCHER_LAUNCH_OMNI_SUCCESSFUL_META)
}
}
override fun isAllAppsButtonHapticFeedbackEnabled(context: Context): Boolean {
return longPressAllAppsToStartContextualSearch(context)
}
}
}
open fun longPressAllAppsToStartContextualSearch(context: Context): Boolean =
ContextualSearchInvoker(context).runContextualSearchInvocationChecksAndLogFailures()
): TaskbarViewCallbacks = TaskbarViewCallbacks(activity, controllers, taskbarView)
companion object {
@JvmStatic
File diff suppressed because it is too large Load Diff
@@ -22,6 +22,7 @@ import android.view.ViewTreeObserver
import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
import com.android.launcher3.util.DisplayController
import com.android.launcher3.views.BaseDragLayer
import com.android.systemui.animation.ViewRootSync
import java.io.PrintWriter
@@ -40,7 +41,7 @@ private const val TEMP_BACKGROUND_WINDOW_TITLE = "VoiceInteractionTaskbarBackgro
class VoiceInteractionWindowController(val context: TaskbarActivityContext) :
TaskbarControllers.LoggableTaskbarController, TaskbarControllers.BackgroundRendererController {
private val isSeparateBackgroundEnabled = !context.isTransientTaskbar && !context.isPhoneMode
private val isSeparateBackgroundEnabled = !DisplayController.isTransientTaskbar(context)
private val taskbarBackgroundRenderer = TaskbarBackgroundRenderer(context)
private val nonTouchableInsetsComputer =
ViewTreeObserver.OnComputeInternalInsetsListener {
@@ -95,7 +96,7 @@ class VoiceInteractionWindowController(val context: TaskbarActivityContext) :
separateWindowLayoutParams =
context.createDefaultWindowLayoutParams(
TYPE_APPLICATION_OVERLAY,
TEMP_BACKGROUND_WINDOW_TITLE,
TEMP_BACKGROUND_WINDOW_TITLE
)
separateWindowLayoutParams?.isSystemApplicationOverlay = true
}
@@ -108,7 +109,7 @@ class VoiceInteractionWindowController(val context: TaskbarActivityContext) :
}
fun setIsVoiceInteractionWindowVisible(visible: Boolean, skipAnim: Boolean) {
if (isVoiceInteractionWindowVisible == visible || context.isPhoneMode) {
if (isVoiceInteractionWindowVisible == visible) {
return
}
isVoiceInteractionWindowVisible = visible
@@ -161,7 +162,7 @@ class VoiceInteractionWindowController(val context: TaskbarActivityContext) :
// First add the temporary window, then hide the overlapping taskbar background.
context.addWindowView(
separateWindowForTaskbarBackground,
separateWindowLayoutParams,
separateWindowLayoutParams
);
{ controllers.taskbarDragLayerController.setIsBackgroundDrawnElsewhere(true) }
} else {
@@ -177,7 +178,7 @@ class VoiceInteractionWindowController(val context: TaskbarActivityContext) :
ViewRootSync.synchronizeNextDraw(
separateWindowForTaskbarBackground!!,
context.dragLayer,
onWindowsSynchronized,
onWindowsSynchronized
)
}
}
@@ -17,10 +17,13 @@ package com.android.launcher3.taskbar.allapps;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
import com.android.launcher3.allapps.ActivityAllAppsContainerView;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
import java.util.Optional;
@@ -43,6 +46,23 @@ public class TaskbarAllAppsContainerView extends
mOnInvalidateHeaderListener = onInvalidateHeaderListener;
}
@Override
protected View inflateSearchBar() {
if (isSearchSupported()) {
return super.inflateSearchBar();
}
// Remove top padding of header, since we do not have any search
mHeader.setPadding(mHeader.getPaddingLeft(), 0,
mHeader.getPaddingRight(), mHeader.getPaddingBottom());
TaskbarAllAppsFallbackSearchContainer searchView = new TaskbarAllAppsFallbackSearchContainer(getContext(),
null);
searchView.setId(R.id.search_container_all_apps);
searchView.setVisibility(GONE);
return searchView;
}
@Override
public void invalidateHeader() {
super.invalidateHeader();
@@ -50,6 +70,11 @@ public class TaskbarAllAppsContainerView extends
OnInvalidateHeaderListener::onInvalidateHeader);
}
@Override
protected boolean isSearchSupported() {
return FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get();
}
@Override
public boolean isInAllApps() {
// All apps is always open
@@ -35,15 +35,21 @@ import com.android.launcher3.util.PackageUserKey;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
/**
* Handles the all apps overlay window initialization, updates, and its data.
* <p>
* All apps is in an application overlay window instead of taskbar's navigation bar panel window,
* because a navigation bar panel is higher than UI components that all apps should be below such as
* All apps is in an application overlay window instead of taskbar's navigation
* bar panel window,
* because a navigation bar panel is higher than UI components that all apps
* should be below such as
* the notification tray.
* <p>
* The all apps window is created and destroyed upon opening and closing all apps, respectively.
* Application data may be bound while the window does not exist, so this controller will store
* The all apps window is created and destroyed upon opening and closing all
* apps, respectively.
* Application data may be bound while the window does not exist, so this
* controller will store
* the models for the next all apps session.
*/
public final class TaskbarAllAppsController {
@@ -69,7 +75,8 @@ public final class TaskbarAllAppsController {
mControllers = controllers;
/*
* Recreate All Apps if it was open in the previous Taskbar instance (e.g. the configuration
* Recreate All Apps if it was open in the previous Taskbar instance (e.g. the
* configuration
* changed).
*/
if (allAppsVisible) {
@@ -119,12 +126,25 @@ public final class TaskbarAllAppsController {
mZeroStateSearchSuggestions = zeroStateSearchSuggestions;
}
/** Toggles visibility of {@link TaskbarAllAppsContainerView} in the overlay window. */
/** Updates the current notification dots. */
public void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
if (mAppsView != null) {
mAppsView.getAppsStore().updateNotificationDots(updatedDots);
}
}
/**
* Toggles visibility of {@link TaskbarAllAppsContainerView} in the overlay
* window.
*/
public void toggle() {
toggle(false);
}
/** Toggles visibility of {@link TaskbarAllAppsContainerView} with the keyboard for search. */
/**
* Toggles visibility of {@link TaskbarAllAppsContainerView} with the keyboard
* for search.
*/
public void toggleSearch() {
toggle(true);
}
@@ -133,7 +153,6 @@ public final class TaskbarAllAppsController {
if (isOpen()) {
mSlideInView.close(true);
} else {
mControllers.taskbarPopupController.maybeCloseMultiInstanceMenu();
show(true, showKeyboard);
}
}
@@ -151,6 +170,11 @@ public final class TaskbarAllAppsController {
if (mAppsView != null) {
return;
}
// mControllers and getSharedState should never be null here. Do not handle
// null-pointer
// to catch invalid states.
mControllers.getSharedState().allAppsVisible = true;
mOverlayContext = mControllers.taskbarOverlayController.requestWindow();
// Initialize search session for All Apps.
@@ -164,8 +188,10 @@ public final class TaskbarAllAppsController {
mSlideInView = (TaskbarAllAppsSlideInView) mOverlayContext.getLayoutInflater().inflate(
R.layout.taskbar_all_apps_sheet, mOverlayContext.getDragLayer(), false);
// Ensures All Apps gets touch events in case it is not the top floating view. Floating
// views above it may not be able to intercept the touch, so All Apps should try to.
// Ensures All Apps gets touch events in case it is not the top floating view.
// Floating
// views above it may not be able to intercept the touch, so All Apps should try
// to.
mOverlayContext.getDragLayer().addTouchController(mSlideInView);
mSlideInView.addOnCloseListener(this::cleanUpOverlay);
TaskbarAllAppsViewController viewController = new TaskbarAllAppsViewController(
@@ -182,15 +208,18 @@ public final class TaskbarAllAppsController {
.findFixedRowByType(PredictionRowView.class)
.setPredictedApps(mPredictedApps);
// 1 alternative that would be more work:
// Create a shared drag layer between taskbar and taskbarAllApps so that when dragging
// starts and taskbarAllApps can close, but the drag layer that the view is being dragged in
// Create a shared drag layer between taskbar and taskbarAllApps so that when
// dragging
// starts and taskbarAllApps can close, but the drag layer that the view is
// being dragged in
// doesn't also close
mOverlayContext.getDragController().setDisallowGlobalDrag(mDisallowGlobalDrag);
mOverlayContext.getDragController().setDisallowLongClick(mDisallowLongClick);
}
private void cleanUpOverlay() {
// Floating search bar is added to the drag layer in ActivityAllAppsContainerView onAttach;
// Floating search bar is added to the drag layer in
// ActivityAllAppsContainerView onAttach;
// removed here as this is a special case that we remove the all apps panel.
if (mAppsView != null && mOverlayContext != null
&& mAppsView.getSearchUiDelegate().isSearchBarFloating()) {
@@ -210,20 +239,17 @@ public final class TaskbarAllAppsController {
mAppsView = null;
}
@Nullable
public TaskbarAllAppsContainerView getAppsView() {
return mAppsView;
}
@VisibleForTesting
public int getTaskbarAllAppsTopPadding() {
// Allow null-pointer since this should only be null if the apps view is not showing.
// Allow null-pointer since this should only be null if the apps view is not
// showing.
return mAppsView.getActiveRecyclerView().getClipBounds().top;
}
@VisibleForTesting
public int getTaskbarAllAppsScroll() {
// Allow null-pointer since this should only be null if the apps view is not showing.
// Allow null-pointer since this should only be null if the apps view is not
// showing.
return mAppsView.getActiveRecyclerView().computeVerticalScrollOffset();
}
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.taskbar.allapps;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.android.launcher3.ExtendedEditText;
import com.android.launcher3.allapps.ActivityAllAppsContainerView;
import com.android.launcher3.allapps.SearchUiManager;
/** Empty search container for Taskbar All Apps used as a fallback if search is not supported. */
public class TaskbarAllAppsFallbackSearchContainer extends View implements SearchUiManager {
public TaskbarAllAppsFallbackSearchContainer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TaskbarAllAppsFallbackSearchContainer(
Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void initializeSearch(ActivityAllAppsContainerView<?> containerView) {
// Do nothing.
}
@Override
public void resetSearch() {
// Do nothing.
}
@Nullable
@Override
public ExtendedEditText getEditText() {
return null;
}
}
@@ -15,9 +15,8 @@
*/
package com.android.launcher3.taskbar.allapps;
import static com.android.app.animation.Interpolators.DECELERATED_EASE;
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.launcher3.Flags.enablePredictiveBackGesture;
import static com.android.launcher3.touch.AllAppsSwipeController.ALL_APPS_FADE_MANUAL;
import static com.android.launcher3.touch.AllAppsSwipeController.SCRIM_FADE_MANUAL;
@@ -35,16 +34,14 @@ import android.window.OnBackInvokedDispatcher;
import androidx.annotation.Nullable;
import app.lawnchair.theme.color.tokens.ColorTokens;
import app.lawnchair.util.LawnchairUtilsKt;
import com.android.app.animation.Interpolators;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.taskbar.allapps.TaskbarAllAppsViewController.TaskbarAllAppsCallbacks;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
import com.android.launcher3.util.Themes;
@@ -54,11 +51,9 @@ import com.android.launcher3.views.AbstractSlideInView;
public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverlayContext>
implements Insettable, DeviceProfile.OnDeviceProfileChangeListener {
private final Handler mHandler;
private final int mMaxBlurRadius;
private TaskbarAllAppsContainerView mAppsView;
private float mShiftRange;
private int mBlurRadius;
private @Nullable Runnable mShowOnFullyAttachedToWindowRunnable;
// Initialized in init.
@@ -72,8 +67,6 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverla
int defStyleAttr) {
super(context, attrs, defStyleAttr);
mHandler = new Handler(Looper.myLooper());
mMaxBlurRadius = getResources().getDimensionPixelSize(
R.dimen.max_depth_blur_radius_enhanced);
}
void init(TaskbarAllAppsCallbacks callbacks) {
@@ -109,7 +102,6 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverla
if (!animate) {
mAllAppsCallbacks.onAllAppsTransitionEnd(true);
setTranslationShift(TRANSLATION_SHIFT_OPENED);
mBlurRadius = mMaxBlurRadius;
return;
}
@@ -129,21 +121,11 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverla
final boolean isOpening = mToTranslationShift == TRANSLATION_SHIFT_OPENED;
if (mActivityContext.getDeviceProfile().isPhone) {
final Interpolator allAppsFadeInterpolator =
isOpening ? ALL_APPS_FADE_MANUAL : Interpolators.reverse(ALL_APPS_FADE_MANUAL);
final Interpolator allAppsFadeInterpolator = isOpening ? ALL_APPS_FADE_MANUAL
: Interpolators.reverse(ALL_APPS_FADE_MANUAL);
animation.setViewAlpha(mAppsView, 1 - mToTranslationShift, allAppsFadeInterpolator);
}
if (Flags.allAppsBlur()) {
Interpolator blurInterpolator = isOpening ? LINEAR : DECELERATED_EASE;
animation.addOnFrameListener(a -> {
float blurProgress =
isOpening ? a.getAnimatedFraction() : 1 - a.getAnimatedFraction();
mBlurRadius =
(int) (mMaxBlurRadius * blurInterpolator.getInterpolation(blurProgress));
});
}
mAllAppsCallbacks.onAllAppsAnimationPending(animation, isOpening);
}
@@ -200,7 +182,9 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverla
mContent = mAppsView;
// Setup header protection for search bar, if enabled.
mAppsView.setOnInvalidateHeaderListener(this::invalidate);
if (FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
mAppsView.setOnInvalidateHeaderListener(this::invalidate);
}
DeviceProfile dp = mActivityContext.getDeviceProfile();
setShiftRange(dp.allAppsShiftRange);
@@ -210,13 +194,16 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverla
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mActivityContext.addOnDeviceProfileChangeListener(this);
mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(mViewOutlineProvider);
mAppsView.getAppsRecyclerViewContainer().setClipToOutline(true);
if (!Utilities.ATLEAST_U) return;
OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
if (dispatcher != null) {
dispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT, this);
if (enablePredictiveBackGesture()) {
mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(mViewOutlineProvider);
mAppsView.getAppsRecyclerViewContainer().setClipToOutline(true);
OnBackInvokedDispatcher dispatcher;
if (!Utilities.ATLEAST_U) return;
dispatcher = findOnBackInvokedDispatcher();
if (dispatcher != null) {
dispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT, null);
}
}
}
@@ -224,25 +211,32 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverla
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mActivityContext.removeOnDeviceProfileChangeListener(this);
mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(null);
mAppsView.getAppsRecyclerViewContainer().setClipToOutline(false);
if (!Utilities.ATLEAST_U) return;
OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
if (dispatcher != null) {
dispatcher.unregisterOnBackInvokedCallback(this);
if (enablePredictiveBackGesture()) {
mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(null);
mAppsView.getAppsRecyclerViewContainer().setClipToOutline(false);
OnBackInvokedDispatcher dispatcher;
if (!Utilities.ATLEAST_U) return;
dispatcher = findOnBackInvokedDispatcher();
if (dispatcher != null && Utilities.ATLEAST_T) {
dispatcher.unregisterOnBackInvokedCallback(null);
}
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
// We should call drawOnScrimWithBottomOffset() rather than drawOnScrimWithScale(). Because
// for taskbar all apps, the scrim view is a child view of AbstractSlideInView. Thus scaling
// down in AbstractSlideInView#onScaleProgressChanged() with SCALE_PROPERTY has already
// done the job - there is no need to re-apply scale effect here. But it also means we need
// to pass extra bottom offset to background scrim to fill the bottom gap during predictive
// We should call drawOnScrimWithBottomOffset() rather than
// drawOnScrimWithScale(). Because
// for taskbar all apps, the scrim view is a child view of AbstractSlideInView.
// Thus scaling
// down in AbstractSlideInView#onScaleProgressChanged() with SCALE_PROPERTY has
// already
// done the job - there is no need to re-apply scale effect here. But it also
// means we need
// to pass extra bottom offset to background scrim to fill the bottom gap during
// predictive
// back swipe.
mAppsView.drawOnScrimWithBottomOffset(canvas, getBottomOffsetPx());
mActivityContext.getOverlayController().setBackgroundBlurRadius(mBlurRadius);
super.dispatchDraw(canvas);
}
@@ -254,13 +248,9 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverla
@Override
protected int getScrimColor(Context context) {
if (!mActivityContext.getDeviceProfile().shouldShowAllAppsOnSheet()) {
return LawnchairUtilsKt.getAllAppsScrimColor(context);
}
if (Flags.allAppsBlur()) {
return LawnchairUtilsKt.getAllAppsScrimColorOverBlur(context);
}
return ColorTokens.WidgetsPickerScrim.resolveColor(context);
return mActivityContext.getDeviceProfile().isPhone
? Themes.getAttrColor(context, R.attr.allAppsScrimColor)
: context.getColor(R.color.widgets_picker_scrim);
}
@Override
@@ -282,7 +272,6 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverla
public void onDeviceProfileChanged(DeviceProfile dp) {
setShiftRange(dp.allAppsShiftRange);
setTranslationShift(TRANSLATION_SHIFT_OPENED);
mBlurRadius = mMaxBlurRadius;
}
private void setShiftRange(float shiftRange) {
@@ -300,8 +289,10 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverla
}
/**
* In taskbar all apps search mode, we should scale down content inside all apps, rather
* than the whole all apps bottom sheet, to indicate we will navigate back within the all apps.
* In taskbar all apps search mode, we should scale down content inside all
* apps, rather
* than the whole all apps bottom sheet, to indicate we will navigate back
* within the all apps.
*/
@Override
public boolean shouldAnimateContentViewInBackSwipe() {
@@ -318,7 +309,8 @@ public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverla
@Override
public void onBackInvoked() {
if (mAllAppsCallbacks.handleSearchBackInvoked()) {
// We need to scale back taskbar all apps if we navigate back within search inside all
// We need to scale back taskbar all apps if we navigate back within search
// inside all
// apps
post(this::animateSwipeToDismissProgressToStart);
} else {
@@ -31,6 +31,7 @@ import com.android.launcher3.taskbar.TaskbarSharedState;
import com.android.launcher3.taskbar.TaskbarStashController;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
import com.android.launcher3.util.DisplayController;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import java.util.Optional;
@@ -89,7 +90,7 @@ final class TaskbarAllAppsViewController {
}
private void setUpTaskbarStashing() {
if (mContext.isTransientTaskbar()) {
if (DisplayController.isTransientTaskbar(mContext)) {
mTaskbarStashController.updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, true);
mTaskbarStashController.applyState();
}
@@ -102,7 +103,7 @@ final class TaskbarAllAppsViewController {
AbstractFloatingView.closeOpenContainer(
mContext, AbstractFloatingView.TYPE_ACTION_POPUP);
if (mContext.isTransientTaskbar()) {
if (DisplayController.isTransientTaskbar(mContext)) {
mTaskbarStashController.updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, false);
mTaskbarStashController.applyState();
}
@@ -21,6 +21,7 @@ import android.view.View
import com.android.launcher3.R
import com.android.launcher3.allapps.AllAppsTransitionListener
import com.android.launcher3.anim.PendingAnimation
import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.dragndrop.DragOptions.PreDragCondition
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.util.ResourceBasedOverride
@@ -60,11 +61,16 @@ open class TaskbarSearchSessionController : ResourceBasedOverride, AllAppsTransi
companion object {
@JvmStatic
fun newInstance(context: Context): TaskbarSearchSessionController =
Overrides.getObject(
fun newInstance(context: Context): TaskbarSearchSessionController {
if (!FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
return TaskbarSearchSessionController()
}
return Overrides.getObject(
TaskbarSearchSessionController::class.java,
context,
R.string.taskbar_search_session_controller_class,
)
}
}
}
@@ -43,14 +43,10 @@ class BubbleBarBackground(context: Context, private var backgroundHeight: Float)
private val arrowTipRadius: Float
private val arrowVisibleHeight: Float
private val strokeAlpha: Int
private val strokeColor: Int
private val strokeColorDropTarget: Int
private val shadowAlpha: Int
private val shadowBlur: Float
private val keyShadowDistance: Float
private val shadowAlpha: Float
private var shadowBlur = 0f
private var keyShadowDistance = 0f
private var arrowHeightFraction = 1f
private var isShowingDropTarget: Boolean = false
var arrowPositionX: Float = 0f
private set
@@ -75,27 +71,6 @@ class BubbleBarBackground(context: Context, private var backgroundHeight: Float)
}
}
/**
* Scale of the background in the x direction. Pivot is at the left edge if [anchorLeft] is
* `true` and at the right edge if it is `false`
*/
var scaleX: Float = 1f
set(value) {
if (field != value) {
field = value
invalidateSelf()
}
}
/** Scale of the background in the y direction. Pivot is at the bottom edge. */
var scaleY: Float = 1f
set(value) {
if (field != value) {
field = value
invalidateSelf()
}
}
init {
val res = context.resources
// configure fill paint
@@ -103,21 +78,19 @@ class BubbleBarBackground(context: Context, private var backgroundHeight: Float)
fillPaint.flags = Paint.ANTI_ALIAS_FLAG
fillPaint.style = Paint.Style.FILL
// configure stroke paint
strokeColor = context.getColor(R.color.taskbar_stroke)
strokeColorDropTarget = context.getColor(com.android.internal.R.color.system_primary_fixed)
strokePaint.color = strokeColor
strokePaint.color = context.getColor(R.color.taskbar_stroke)
strokePaint.flags = Paint.ANTI_ALIAS_FLAG
strokePaint.style = Paint.Style.STROKE
strokePaint.strokeWidth = res.getDimension(R.dimen.transient_taskbar_stroke_width)
// apply theme alpha attributes
if (Utilities.isDarkTheme(context)) {
strokeAlpha = DARK_THEME_STROKE_ALPHA
strokePaint.alpha = DARK_THEME_STROKE_ALPHA
shadowAlpha = DARK_THEME_SHADOW_ALPHA
} else {
strokeAlpha = LIGHT_THEME_STROKE_ALPHA
strokePaint.alpha = LIGHT_THEME_STROKE_ALPHA
shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
}
strokePaint.alpha = strokeAlpha
shadowBlur = res.getDimension(R.dimen.transient_taskbar_shadow_blur)
keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance)
arrowWidth = res.getDimension(R.dimen.bubblebar_pointer_width)
@@ -138,28 +111,25 @@ class BubbleBarBackground(context: Context, private var backgroundHeight: Float)
override fun draw(canvas: Canvas) {
canvas.save()
// TODO (b/277359345): Should animate the alpha similar to taskbar (see TaskbarDragLayer)
// Draw shadows.
val newShadowAlpha =
mapToRange(fillPaint.alpha, 0, 255, 0, shadowAlpha, Interpolators.LINEAR)
mapToRange(fillPaint.alpha.toFloat(), 0f, 255f, 0f, shadowAlpha, Interpolators.LINEAR)
fillPaint.setShadowLayer(
shadowBlur,
0f,
keyShadowDistance,
setColorAlphaBound(Color.BLACK, newShadowAlpha),
setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
)
// Create background path
val backgroundPath = Path()
val scaledBackgroundHeight = backgroundHeight * scaleY
val scaledWidth = width * scaleX
val topOffset = scaledBackgroundHeight - bounds.height().toFloat()
val topOffset = backgroundHeight - bounds.height().toFloat()
val radius = backgroundHeight / 2f
val left = bounds.left + (if (anchorLeft) 0f else bounds.width().toFloat() - width)
val right = bounds.left + (if (anchorLeft) width else bounds.width().toFloat())
val top = bounds.top - topOffset + arrowVisibleHeight
val left = bounds.left + (if (anchorLeft) 0f else bounds.width().toFloat() - scaledWidth)
val right = bounds.left + (if (anchorLeft) scaledWidth else bounds.width().toFloat())
// Calculate top with scaled heights for background and arrow to align with stash handle
val top = bounds.bottom - scaledBackgroundHeight + getScaledArrowVisibleHeight()
val bottom = bounds.bottom.toFloat()
val bottom = bounds.top + bounds.height().toFloat()
backgroundPath.addRoundRect(left, top, right, bottom, radius, radius, Path.Direction.CW)
addArrowPathIfNeeded(backgroundPath, topOffset)
@@ -172,20 +142,19 @@ class BubbleBarBackground(context: Context, private var backgroundHeight: Float)
private fun addArrowPathIfNeeded(sourcePath: Path, topOffset: Float) {
if (!showingArrow || arrowHeightFraction <= 0) return
val arrowPath = Path()
val scaledHeight = getScaledArrowHeight()
RoundedArrowDrawable.addDownPointingRoundedTriangleToPath(
arrowWidth,
scaledHeight,
arrowHeight,
arrowTipRadius,
arrowPath,
arrowPath
)
// flip it horizontally
val pathTransform = Matrix()
pathTransform.setRotate(180f, arrowWidth * 0.5f, scaledHeight * 0.5f)
pathTransform.setRotate(180f, arrowWidth * 0.5f, arrowHeight * 0.5f)
arrowPath.transform(pathTransform)
// shift to arrow position
val arrowStart = bounds.left + arrowPositionX - (arrowWidth / 2f)
val arrowTop = (1 - arrowHeightFraction) * getScaledArrowVisibleHeight() - topOffset
val arrowTop = (1 - arrowHeightFraction) * arrowVisibleHeight - topOffset
arrowPath.offset(arrowStart, arrowTop)
// union with rectangle
sourcePath.op(arrowPath, Path.Op.UNION)
@@ -201,7 +170,6 @@ class BubbleBarBackground(context: Context, private var backgroundHeight: Float)
override fun setAlpha(alpha: Int) {
fillPaint.alpha = alpha
strokePaint.alpha = mapToRange(alpha, 0, 255, 0, strokeAlpha, Interpolators.LINEAR)
invalidateSelf()
}
@@ -215,7 +183,6 @@ class BubbleBarBackground(context: Context, private var backgroundHeight: Float)
fun setBackgroundHeight(newHeight: Float) {
backgroundHeight = newHeight
invalidateSelf()
}
/**
@@ -232,34 +199,10 @@ class BubbleBarBackground(context: Context, private var backgroundHeight: Float)
invalidateSelf()
}
private fun getScaledArrowHeight(): Float {
return arrowHeight * scaleY
}
private fun getScaledArrowVisibleHeight(): Float {
return max(0f, getScaledArrowHeight() - (arrowHeight - arrowVisibleHeight))
}
/** Set whether the background should show the drop target */
fun showDropTarget(isDropTarget: Boolean) {
if (isShowingDropTarget == isDropTarget) {
return
}
isShowingDropTarget = isDropTarget
val strokeColor = if (isDropTarget) strokeColorDropTarget else strokeColor
val alpha = if (isDropTarget) DRAG_STROKE_ALPHA else strokeAlpha
strokePaint.color = strokeColor
strokePaint.alpha = alpha
invalidateSelf()
}
fun isShowingDropTarget() = isShowingDropTarget
companion object {
private const val DARK_THEME_STROKE_ALPHA = 51
private const val LIGHT_THEME_STROKE_ALPHA = 41
private const val DRAG_STROKE_ALPHA = 255
private const val DARK_THEME_SHADOW_ALPHA = 51
private const val LIGHT_THEME_SHADOW_ALPHA = 25
private const val DARK_THEME_SHADOW_ALPHA = 51f
private const val LIGHT_THEME_SHADOW_ALPHA = 25f
}
}
@@ -15,11 +15,17 @@
*/
package com.android.launcher3.taskbar.bubbles;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static com.android.launcher3.icons.FastBitmapDrawable.WHITE_SCRIM_ALPHA;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
@@ -29,42 +35,65 @@ import static java.util.stream.Collectors.toList;
import android.annotation.BinderThread;
import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Bundle;
import android.os.Process;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Log;
import android.util.PathParser;
import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import com.android.launcher3.taskbar.TaskbarSharedState;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
import com.android.internal.graphics.ColorUtils;
import com.android.launcher3.R;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.BubbleIconFactory;
import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.Executors.SimpleThreadFactory;
import com.android.quickstep.SystemUiProxy;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.wm.shell.Flags;
import com.android.wm.shell.bubbles.IBubblesListener;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
import com.android.wm.shell.shared.bubbles.BubbleInfo;
import com.android.wm.shell.shared.bubbles.RemovedBubble;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
import com.android.wm.shell.common.bubbles.BubbleInfo;
import com.android.wm.shell.common.bubbles.RemovedBubble;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* This registers a listener with SysUIProxy to get information about changes to the bubble
* stack state from WMShell (SysUI). The controller is also responsible for loading the necessary
* This registers a listener with SysUIProxy to get information about changes to
* the bubble
* stack state from WMShell (SysUI). The controller is also responsible for
* loading the necessary
* information to render each of the bubbles & dispatches changes to
* {@link BubbleBarViewController} which will then update {@link BubbleBarView} as needed.
* {@link BubbleBarViewController} which will then update {@link BubbleBarView}
* as needed.
*
* <p>For details around the behavior of the bubble bar, see {@link BubbleBarView}.
* <p>
* For details around the behavior of the bubble bar, see {@link BubbleBarView}.
*/
public class BubbleBarController extends IBubblesListener.Stub {
@@ -72,7 +101,8 @@ public class BubbleBarController extends IBubblesListener.Stub {
private static final boolean DEBUG = false;
/**
* Determines whether bubbles can be shown in the bubble bar. This value updates when the
* Determines whether bubbles can be shown in the bubble bar. This value updates
* when the
* taskbar is recreated.
*
* @see #onTaskbarRecreated()
@@ -85,7 +115,10 @@ public class BubbleBarController extends IBubblesListener.Stub {
return sBubbleBarEnabled;
}
/** Re-reads the value of the flag from SystemProperties when taskbar is recreated. */
/**
* Re-reads the value of the flag from SystemProperties when taskbar is
* recreated.
*/
public static void onTaskbarRecreated() {
sBubbleBarEnabled = Flags.enableBubbleBar()
|| SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
@@ -94,9 +127,10 @@ public class BubbleBarController extends IBubblesListener.Stub {
private static final long MASK_HIDE_BUBBLE_BAR = SYSUI_STATE_BOUNCER_SHOWING
| SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
| SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
| SYSUI_STATE_IME_VISIBLE
| SYSUI_STATE_IME_SHOWING
| SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
| SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
| SYSUI_STATE_QUICK_SETTINGS_EXPANDED
| SYSUI_STATE_IME_SWITCHER_SHOWING;
private static final long MASK_HIDE_HANDLE_VIEW = SYSUI_STATE_BOUNCER_SHOWING
| SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
@@ -109,29 +143,28 @@ public class BubbleBarController extends IBubblesListener.Stub {
private final Context mContext;
private final BubbleBarView mBarView;
private final ArrayMap<String, BubbleBarBubble> mBubbles = new ArrayMap<>();
private final ArrayMap<String, BubbleBarBubble> mSuppressedBubbles = new ArrayMap<>();
private static final Executor BUBBLE_STATE_EXECUTOR = Executors.newSingleThreadExecutor(
new SimpleThreadFactory("BubbleStateUpdates-", THREAD_PRIORITY_BACKGROUND));
private final LauncherApps mLauncherApps;
private final BubbleIconFactory mIconFactory;
private final SystemUiProxy mSystemUiProxy;
private BubbleBarItem mSelectedBubble;
private BubbleBarOverflow mOverflowBubble;
private TaskbarSharedState mSharedState;
private ImeVisibilityChecker mImeVisibilityChecker;
private BubbleBarViewController mBubbleBarViewController;
private BubbleStashController mBubbleStashController;
private Optional<BubbleStashedHandleViewController> mBubbleStashedHandleViewController;
private BubbleStashedHandleViewController mBubbleStashedHandleViewController;
private BubblePinController mBubblePinController;
private BubbleCreator mBubbleCreator;
private BubbleBarLocationListener mBubbleBarLocationListener;
// Cache last sent top coordinate to avoid sending duplicate updates to shell
private int mLastSentBubbleBarTop;
private boolean mIsImeVisible = false;
/**
* Similar to {@link BubbleBarUpdate} but rather than {@link BubbleInfo}s it uses
* Similar to {@link BubbleBarUpdate} but rather than {@link BubbleInfo}s it
* uses
* {@link BubbleBarBubble}s so that it can be used to update the views.
*/
private static class BubbleBarViewUpdate {
@@ -146,8 +179,6 @@ public class BubbleBarController extends IBubblesListener.Stub {
List<RemovedBubble> removedBubbles;
List<String> bubbleKeysInOrder;
Point expandedViewDropTargetSize;
boolean showOverflow;
boolean showOverflowChanged;
// These need to be loaded in the background
BubbleBarBubble addedBubble;
@@ -166,8 +197,6 @@ public class BubbleBarController extends IBubblesListener.Stub {
removedBubbles = update.removedBubbles;
bubbleKeysInOrder = update.bubbleKeysInOrder;
expandedViewDropTargetSize = update.expandedViewDropTargetSize;
showOverflow = update.showOverflow;
showOverflowChanged = update.showOverflowChanged;
}
}
@@ -176,74 +205,81 @@ public class BubbleBarController extends IBubblesListener.Stub {
mBarView = bubbleView; // Need the view for inflating bubble views.
mSystemUiProxy = SystemUiProxy.INSTANCE.get(context);
if (sBubbleBarEnabled) {
mSystemUiProxy.setBubblesListener(this);
}
mLauncherApps = context.getSystemService(LauncherApps.class);
mIconFactory = new BubbleIconFactory(context,
context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size),
context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size),
context.getResources().getColor(R.color.important_conversation),
context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.importance_ring_stroke_width));
}
public void onDestroy() {
mSystemUiProxy.setBubblesListener(null);
// Saves bubble bar state
BubbleInfo[] bubbleInfoItems = new BubbleInfo[mBubbles.size()];
mBubbles.values().forEach(bubbleBarBubble -> {
int index = mBubbleBarViewController.bubbleViewIndex(bubbleBarBubble.getView());
if (index < 0 || index >= bubbleInfoItems.length) {
Log.e(TAG, "Found improper index: " + index + " for " + bubbleBarBubble);
} else {
bubbleInfoItems[index] = bubbleBarBubble.getInfo();
}
});
mSharedState.bubbleInfoItems = Arrays.asList(bubbleInfoItems);
mSharedState.suppressedBubbleInfoItems = new ArrayList<>(mSuppressedBubbles.size());
for (int i = 0; i < mSuppressedBubbles.size(); i++) {
mSharedState.suppressedBubbleInfoItems.add(mSuppressedBubbles.valueAt(i).getInfo());
}
}
/** Initializes controllers. */
public void init(BubbleControllers bubbleControllers,
BubbleBarLocationListener bubbleBarLocationListener,
TaskbarSharedState sharedState) {
mSharedState = sharedState;
ImeVisibilityChecker imeVisibilityChecker) {
mImeVisibilityChecker = imeVisibilityChecker;
mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
mBubbleStashController = bubbleControllers.bubbleStashController;
mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
mBubblePinController = bubbleControllers.bubblePinController;
mBubbleCreator = bubbleControllers.bubbleCreator;
mBubbleBarLocationListener = bubbleBarLocationListener;
bubbleControllers.runAfterInit(() -> {
restoreSavedState(sharedState);
mBubbleBarViewController.setHiddenForBubbles(
!sBubbleBarEnabled || mBubbles.isEmpty());
mBubbleStashedHandleViewController.ifPresent(
controller -> controller.setHiddenForBubbles(
!sBubbleBarEnabled || mBubbles.isEmpty()));
mBubbleStashedHandleViewController.setHiddenForBubbles(
!sBubbleBarEnabled || mBubbles.isEmpty());
mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse(
key -> setSelectedBubbleInternal(mBubbles.get(key)));
mBubbleBarViewController.setBoundsChangeListener(this::onBubbleBarBoundsChanged);
mBubbleBarLocationListener.onBubbleBarLocationUpdated(
mBubbleBarViewController.getBubbleBarLocation());
if (sBubbleBarEnabled) {
mSystemUiProxy.setBubblesListener(this);
}
});
}
/**
* Updates the bubble bar, handle bar, and stash controllers based on sysui state flags.
* Creates and adds the overflow bubble to the bubble bar if it hasn't been
* created yet.
*
* <p>
* This should be called on the {@link #BUBBLE_STATE_EXECUTOR} executor to avoid
* inflating
* the overflow multiple times.
*/
private void createAndAddOverflowIfNeeded() {
if (mOverflowBubble == null) {
BubbleBarOverflow overflow = createOverflow(mContext);
MAIN_EXECUTOR.execute(() -> {
// we're on the main executor now, so check that the overflow hasn't been
// created
// again to avoid races.
if (mOverflowBubble == null) {
mBubbleBarViewController.addBubble(
overflow, /* isExpanding= */ false, /* suppressAnimation= */ true);
mOverflowBubble = overflow;
}
});
}
}
/**
* Updates the bubble bar, handle bar, and stash controllers based on sysui
* state flags.
*/
public void updateStateForSysuiFlags(@SystemUiStateFlags long flags) {
boolean hideBubbleBar = (flags & MASK_HIDE_BUBBLE_BAR) != 0;
mBubbleBarViewController.setHiddenForSysui(hideBubbleBar);
boolean hideHandleView = (flags & MASK_HIDE_HANDLE_VIEW) != 0;
mBubbleStashedHandleViewController.ifPresent(
controller -> controller.setHiddenForSysui(hideHandleView));
mBubbleStashedHandleViewController.setHiddenForSysui(hideHandleView);
boolean sysuiLocked = (flags & MASK_SYSUI_LOCKED) != 0;
mBubbleStashController.setSysuiLocked(sysuiLocked);
mIsImeVisible = (flags & SYSUI_STATE_IME_VISIBLE) != 0;
if (mIsImeVisible) {
mBubbleBarViewController.onImeVisible();
}
mBubbleStashController.onSysuiLockedStateChange(sysuiLocked);
}
//
@@ -261,24 +297,20 @@ public class BubbleBarController extends IBubblesListener.Stub {
|| !update.currentBubbleList.isEmpty()) {
// We have bubbles to load
BUBBLE_STATE_EXECUTOR.execute(() -> {
createAndAddOverflowIfNeeded();
if (update.addedBubble != null) {
viewUpdate.addedBubble = mBubbleCreator.populateBubble(mContext,
update.addedBubble,
mBarView,
viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView,
null /* existingBubble */);
}
if (update.updatedBubble != null) {
BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey());
viewUpdate.updatedBubble =
mBubbleCreator.populateBubble(mContext, update.updatedBubble,
mBarView,
existingBubble);
viewUpdate.updatedBubble = populateBubble(mContext, update.updatedBubble, mBarView,
existingBubble);
}
if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) {
List<BubbleBarBubble> currentBubbles = new ArrayList<>();
for (int i = 0; i < update.currentBubbleList.size(); i++) {
BubbleBarBubble b = mBubbleCreator.populateBubble(mContext,
update.currentBubbleList.get(i), mBarView,
BubbleBarBubble b = populateBubble(mContext, update.currentBubbleList.get(i), mBarView,
null /* existingBubble */);
currentBubbles.add(b);
}
@@ -293,61 +325,92 @@ public class BubbleBarController extends IBubblesListener.Stub {
}
}
private void restoreSavedState(TaskbarSharedState sharedState) {
if (sharedState.bubbleBarLocation != null) {
updateBubbleBarLocationInternal(sharedState.bubbleBarLocation);
}
restoreSavedBubbles(sharedState.bubbleInfoItems);
restoreSuppressed(sharedState.suppressedBubbleInfoItems);
}
private void restoreSavedBubbles(List<BubbleInfo> bubbleInfos) {
if (bubbleInfos == null || bubbleInfos.isEmpty()) return;
// Iterate in reverse because new bubbles are added in front and the list is in order.
for (int i = bubbleInfos.size() - 1; i >= 0; i--) {
BubbleBarBubble bubble = mBubbleCreator.populateBubble(mContext,
bubbleInfos.get(i), mBarView, /* existingBubble = */ null);
if (bubble == null) {
Log.e(TAG, "Could not instantiate BubbleBarBubble for " + bubbleInfos.get(i));
continue;
}
addBubbleInternally(bubble, /* isExpanding= */ false, /* suppressAnimation= */ true);
}
}
private void restoreSuppressed(List<BubbleInfo> bubbleInfos) {
if (bubbleInfos == null || bubbleInfos.isEmpty()) return;
for (BubbleInfo bubbleInfo : bubbleInfos.reversed()) {
BubbleBarBubble bb = mBubbleCreator.populateBubble(mContext, bubbleInfo,
mBarView, /* existingBubble= */
null);
if (bb != null) {
mSuppressedBubbles.put(bb.getKey(), bb);
}
}
}
private void applyViewChanges(BubbleBarViewUpdate update) {
final boolean isCollapsed = (update.expandedChanged && !update.expanded)
|| (!update.expandedChanged && !mBubbleBarViewController.isExpanded());
final boolean isExpanding = update.expandedChanged && update.expanded;
// don't animate bubbles if this is the initial state because we may be unfolding or
// enabling gesture nav. also suppress animation if the bubble bar is hidden for sysui e.g.
// don't animate bubbles if this is the initial state because we may be
// unfolding or
// enabling gesture nav. also suppress animation if the bubble bar is hidden for
// sysui e.g.
// the shade is open, or we're locked.
final boolean suppressAnimation =
update.initialState || mBubbleBarViewController.isHiddenForSysui() || mIsImeVisible;
final boolean suppressAnimation = update.initialState || mBubbleBarViewController.isHiddenForSysui()
|| mImeVisibilityChecker.isImeVisible();
if (update.initialState && mSharedState.hasSavedBubbles()) {
// clear restored state
mBubbleBarViewController.removeAllBubbles();
mBubbles.clear();
mBubbleBarViewController.showOverflow(update.showOverflow);
BubbleBarBubble bubbleToSelect = null;
if (!update.removedBubbles.isEmpty()) {
for (int i = 0; i < update.removedBubbles.size(); i++) {
RemovedBubble removedBubble = update.removedBubbles.get(i);
BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey());
if (bubble != null) {
mBubbleBarViewController.removeBubble(bubble);
} else {
Log.w(TAG, "trying to remove bubble that doesn't exist: "
+ removedBubble.getKey());
}
}
}
if (update.addedBubble != null) {
mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, suppressAnimation);
if (isCollapsed) {
// If we're collapsed, the most recently added bubble will be selected.
bubbleToSelect = update.addedBubble;
}
}
if (update.currentBubbles != null && !update.currentBubbles.isEmpty()) {
// Iterate in reverse because new bubbles are added in front and the list is in
// order.
for (int i = update.currentBubbles.size() - 1; i >= 0; i--) {
BubbleBarBubble bubble = update.currentBubbles.get(i);
if (bubble != null) {
mBubbles.put(bubble.getKey(), bubble);
mBubbleBarViewController.addBubble(bubble, isExpanding, suppressAnimation);
if (isCollapsed) {
// If we're collapsed, the most recently added bubble will be selected.
bubbleToSelect = bubble;
}
} else {
Log.w(TAG, "trying to add bubble but null after loading! "
+ update.addedBubble.getKey());
}
}
}
// Adds and removals have happened, update visibility before any other visual
// changes.
mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());
if (mBubbles.isEmpty()) {
// all bubbles were removed. clear the selected bubble
mSelectedBubble = null;
}
if (update.updatedBubble != null) {
// Updates mean the dot state may have changed; any other changes were updated
// in
// the populateBubble step.
BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
// If we're not stashed, we're visible so animate
bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
mBubbleBarViewController.animateBubbleNotification(bb, /* isExpanding= */ false);
}
if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
// Create the new list
List<BubbleBarBubble> newOrder = update.bubbleKeysInOrder.stream()
.map(mBubbles::get).filter(Objects::nonNull).collect(toList());
if (!newOrder.isEmpty()) {
mBubbleBarViewController.reorderBubbles(newOrder);
}
}
if (update.suppressedBubbleKey != null) {
// TODO: (b/273316505) handle suppression
}
if (update.unsuppressedBubbleKey != null) {
// TODO: (b/273316505) handle suppression
}
BubbleBarBubble bubbleToSelect = null;
if (update.selectedBubbleKey != null) {
if (mSelectedBubble == null
|| !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) {
@@ -360,139 +423,6 @@ public class BubbleBarController extends IBubblesListener.Stub {
}
}
}
if (Flags.enableOptionalBubbleOverflow()
&& update.showOverflowChanged && !update.showOverflow && update.addedBubble != null
&& update.removedBubbles.isEmpty()
&& !mBubbles.isEmpty()) {
// A bubble was added from the overflow (& now it's empty / not showing)
mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble, bubbleToSelect);
} else if (update.addedBubble != null && update.removedBubbles.size() == 1) {
// we're adding and removing a bubble at the same time. handle this as a single update.
RemovedBubble removedBubble = update.removedBubbles.get(0);
BubbleBarBubble bubbleToRemove = mBubbles.remove(removedBubble.getKey());
boolean showOverflow = update.showOverflowChanged && update.showOverflow;
if (bubbleToRemove != null) {
mBubbleBarViewController.addBubbleAndRemoveBubble(update.addedBubble,
bubbleToRemove, bubbleToSelect, isExpanding, suppressAnimation,
showOverflow);
} else {
mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
suppressAnimation, bubbleToSelect);
Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey());
}
} else {
boolean overflowNeedsToBeAdded = Flags.enableOptionalBubbleOverflow()
&& update.showOverflowChanged && update.showOverflow;
if (!update.removedBubbles.isEmpty()) {
for (int i = 0; i < update.removedBubbles.size(); i++) {
RemovedBubble removedBubble = update.removedBubbles.get(i);
BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey());
if (bubble != null && overflowNeedsToBeAdded) {
// First removal, show the overflow
overflowNeedsToBeAdded = false;
mBubbleBarViewController.addOverflowAndRemoveBubble(bubble, bubbleToSelect);
} else if (bubble != null) {
mBubbleBarViewController.removeBubble(bubble);
} else {
Log.w(TAG, "trying to remove bubble that doesn't exist: "
+ removedBubble.getKey());
}
}
}
if (update.addedBubble != null) {
mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
suppressAnimation, bubbleToSelect);
}
if (Flags.enableOptionalBubbleOverflow()
&& update.showOverflowChanged
&& update.showOverflow != mBubbleBarViewController.isOverflowAdded()) {
mBubbleBarViewController.showOverflow(update.showOverflow);
}
}
// if a bubble was updated upstream, but removed before the update was received, add it back
if (update.updatedBubble != null && !mBubbles.containsKey(update.updatedBubble.getKey())) {
addBubbleInternally(update.updatedBubble, isExpanding, suppressAnimation);
}
if (update.addedBubble != null && isCollapsed && bubbleToSelect == null) {
// If we're collapsed, the most recently added bubble will be selected.
bubbleToSelect = update.addedBubble;
}
if (update.currentBubbles != null && !update.currentBubbles.isEmpty()) {
// Iterate in reverse because new bubbles are added in front and the list is in order.
for (int i = update.currentBubbles.size() - 1; i >= 0; i--) {
BubbleBarBubble bubble = update.currentBubbles.get(i);
if (bubble != null) {
addBubbleInternally(bubble, isExpanding, suppressAnimation);
if (isCollapsed && bubbleToSelect == null) {
// If we're collapsed, the most recently added bubble will be selected.
bubbleToSelect = bubble;
}
} else {
Log.w(TAG, "trying to add bubble but null after loading! "
+ update.addedBubble.getKey());
}
}
}
if (Flags.enableOptionalBubbleOverflow() && update.initialState && update.showOverflow) {
mBubbleBarViewController.showOverflow(true);
}
if (update.suppressedBubbleKey != null) {
BubbleBarBubble bb = mBubbles.remove(update.suppressedBubbleKey);
if (bb != null) {
mSuppressedBubbles.put(update.suppressedBubbleKey, bb);
mBubbleBarViewController.removeBubble(bb);
}
}
if (update.unsuppressedBubbleKey != null) {
BubbleBarBubble bb = mSuppressedBubbles.remove(update.unsuppressedBubbleKey);
if (bb != null) {
// Unsuppressing an existing bubble should not cause the bar to expand or animate
addBubbleInternally(bb, /* isExpanding= */ false, /* suppressAnimation= */ true);
if (mBubbleBarViewController.isHiddenForNoBubbles()) {
mBubbleBarViewController.setHiddenForBubbles(false);
}
}
}
// Update the visibility if this is the initial state, if there are no bubbles, or if the
// animation is suppressed.
// If this is the initial bubble, the bubble bar will become visible as part of the
// animation.
if (update.initialState || mBubbles.isEmpty() || suppressAnimation) {
mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
}
mBubbleStashedHandleViewController.ifPresent(
controller -> controller.setHiddenForBubbles(mBubbles.isEmpty()));
if (mBubbles.isEmpty()) {
// all bubbles were removed. clear the selected bubble
mSelectedBubble = null;
}
if (update.updatedBubble != null) {
// Updates mean the dot state may have changed; any other changes were updated in
// the populateBubble step.
BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
if (suppressAnimation) {
// since we're not animating this update, we should update the dot visibility here.
bb.getView().updateDotVisibility(/* animate= */ false);
} else {
mBubbleBarViewController.animateBubbleNotification(
bb, /* isExpanding= */ false, /* isUpdate= */ true);
}
}
if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
// Create the new list
List<BubbleBarBubble> newOrder = update.bubbleKeysInOrder.stream()
.map(mBubbles::get).filter(Objects::nonNull).toList();
if (!newOrder.isEmpty()) {
mBubbleBarViewController.reorderBubbles(newOrder);
}
}
if (bubbleToSelect != null) {
setSelectedBubbleInternal(bubbleToSelect);
}
@@ -507,7 +437,6 @@ public class BubbleBarController extends IBubblesListener.Stub {
}
}
if (update.bubbleBarLocation != null) {
mSharedState.bubbleBarLocation = update.bubbleBarLocation;
if (update.bubbleBarLocation != mBubbleBarViewController.getBubbleBarLocation()) {
updateBubbleBarLocationInternal(update.bubbleBarLocation);
}
@@ -517,16 +446,19 @@ public class BubbleBarController extends IBubblesListener.Stub {
}
}
/**
* Removes the given bubble from the backing list of bubbles after it was dismissed by the user.
*/
public void onBubbleDismissed(BubbleView bubble) {
mBubbles.remove(bubble.getBubble().getKey());
}
/** Tells WMShell to show the currently selected bubble. */
public void showSelectedBubble() {
if (getSelectedBubbleKey() != null) {
if (mSelectedBubble instanceof BubbleBarBubble) {
// Because we've visited this bubble, we should suppress the notification.
// This is updated on WMShell side when we show the bubble, but that update
// isn't
// passed to launcher, instead we apply it directly here.
BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo();
info.setFlags(
info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
mSelectedBubble.getView().updateDotVisibility(true /* animate */);
}
mLastSentBubbleBarTop = mBarView.getRestingTopPositionOnScreen();
mSystemUiProxy.showBubble(getSelectedBubbleKey(), mLastSentBubbleBarTop);
} else {
@@ -534,21 +466,27 @@ public class BubbleBarController extends IBubblesListener.Stub {
}
}
/** Updates the currently selected bubble for launcher views and tells WMShell to show it. */
/**
* 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());
if (DEBUG)
Log.w(TAG, "showingSelectedBubble: " + b.getKey());
setSelectedBubbleInternal(b);
showSelectedBubble();
}
/**
* Sets the bubble that should be selected. This notifies the views, it does not notify
* Sets the bubble that should be selected. This notifies the views, it does not
* notify
* WMShell that the selection has changed, that should go through either
* {@link #showSelectedBubble()} or {@link #showAndSelectBubble(BubbleBarItem)}.
*/
private void setSelectedBubbleInternal(BubbleBarItem b) {
if (!Objects.equals(b, mSelectedBubble)) {
if (DEBUG) Log.w(TAG, "selectingBubble: " + b.getKey());
if (DEBUG)
Log.w(TAG, "selectingBubble: " + b.getKey());
mSelectedBubble = b;
mBubbleBarViewController.updateSelectedBubble(mSelectedBubble);
}
@@ -570,57 +508,153 @@ public class BubbleBarController extends IBubblesListener.Stub {
* <p>
* Updates the value locally in Launcher and in WMShell.
*/
public void updateBubbleBarLocation(BubbleBarLocation location,
@BubbleBarLocation.UpdateSource int source) {
public void updateBubbleBarLocation(BubbleBarLocation location) {
updateBubbleBarLocationInternal(location);
mSystemUiProxy.setBubbleBarLocation(location, source);
mSystemUiProxy.setBubbleBarLocation(location);
}
private void updateBubbleBarLocationInternal(BubbleBarLocation location) {
mBubbleBarViewController.setBubbleBarLocation(location);
mBubbleStashController.setBubbleBarLocation(location);
mBubbleBarLocationListener.onBubbleBarLocationUpdated(location);
}
@Override
public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
MAIN_EXECUTOR.execute(
() -> {
mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation);
mBubbleBarLocationListener.onBubbleBarLocationAnimated(bubbleBarLocation);
});
}
@Override
public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) {
MAIN_EXECUTOR.execute(() -> {
mBubbleBarViewController.onDragItemOverBubbleBarDragZone(bubbleBarLocation);
if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) {
mBubbleBarLocationListener.onBubbleBarLocationAnimated(bubbleBarLocation);
}
});
}
@Override
public void onItemDraggedOutsideBubbleBarDropZone() {
MAIN_EXECUTOR.execute(() -> {
if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) {
BubbleBarLocation original = mBubbleBarViewController.getBubbleBarLocation();
mBubbleBarLocationListener.onBubbleBarLocationAnimated(original);
}
mBubbleBarViewController.onItemDraggedOutsideBubbleBarDropZone();
});
}
/** Notifies WMShell to show the expanded view. */
void showExpandedView() {
mSystemUiProxy.showExpandedView();
() -> mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation));
}
//
// Loading data for the bubbles
//
@Nullable
private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv,
@Nullable BubbleBarBubble existingBubble) {
String appName;
Bitmap badgeBitmap;
Bitmap bubbleBitmap;
Path dotPath;
int dotColor;
boolean isImportantConvo = b.isImportantConversation();
ShortcutRequest.QueryResult result = new ShortcutRequest(context,
new UserHandle(b.getUserId()))
.forPackage(b.getPackageName(), b.getShortcutId())
.query(FLAG_MATCH_DYNAMIC
| FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
| FLAG_MATCH_CACHED
| FLAG_GET_PERSONS_DATA);
ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null;
if (shortcutInfo == null) {
Log.w(TAG, "No shortcutInfo found for bubble: " + b.getKey()
+ " with shortcutId: " + b.getShortcutId());
}
ApplicationInfo appInfo;
try {
appInfo = mLauncherApps.getApplicationInfo(
b.getPackageName(),
0,
new UserHandle(b.getUserId()));
} catch (PackageManager.NameNotFoundException e) {
// If we can't find package... don't think we should show the bubble.
Log.w(TAG, "Unable to find packageName: " + b.getPackageName());
return null;
}
if (appInfo == null) {
Log.w(TAG, "Unable to find appInfo: " + b.getPackageName());
return null;
}
PackageManager pm = context.getPackageManager();
appName = String.valueOf(appInfo.loadLabel(pm));
Drawable appIcon = appInfo.loadUnbadgedIcon(pm);
Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(b.getUserId()));
// Badged bubble image
Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo,
b.getIcon());
if (bubbleDrawable == null) {
// Default to app icon
bubbleDrawable = appIcon;
}
BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo);
badgeBitmap = badgeBitmapInfo.icon;
float[] bubbleBitmapScale = new float[1];
bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
// Dot color & placement
Path iconPath = PathParser.createPathFromPathData(
context.getResources().getString(
com.android.internal.R.string.config_icon_mask));
Matrix matrix = new Matrix();
float scale = bubbleBitmapScale[0];
float radius = BubbleView.DEFAULT_PATH_SIZE / 2f;
matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
radius /* pivot y */);
iconPath.transform(matrix);
dotPath = iconPath;
dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
if (existingBubble == null) {
LayoutInflater inflater = LayoutInflater.from(context);
BubbleView bubbleView = (BubbleView) inflater.inflate(
R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
bubbleView.setBubble(bubble);
return bubble;
} else {
// If we already have a bubble (so it already has an inflated view), update it.
existingBubble.setInfo(b);
existingBubble.setBadge(badgeBitmap);
existingBubble.setIcon(bubbleBitmap);
existingBubble.setDotColor(dotColor);
existingBubble.setDotPath(dotPath);
existingBubble.setAppName(appName);
return existingBubble;
}
}
private BubbleBarOverflow createOverflow(Context context) {
Bitmap bitmap = createOverflowBitmap(context);
LayoutInflater inflater = LayoutInflater.from(context);
BubbleView bubbleView = (BubbleView) inflater.inflate(
R.layout.bubblebar_item_view, mBarView, false /* attachToRoot */);
BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
bubbleView.setOverflow(overflow, bitmap);
return overflow;
}
private Bitmap createOverflowBitmap(Context context) {
Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
R.drawable.bubble_ic_overflow_button);
final TypedArray ta = mContext.obtainStyledAttributes(
new int[] {
com.android.internal.R.attr.materialColorOnPrimaryFixed,
com.android.internal.R.attr.materialColorPrimaryFixed
});
int overflowIconColor = ta.getColor(0, Color.WHITE);
int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
ta.recycle();
iconDrawable.setTint(overflowIconColor);
int inset = context.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset);
Drawable foreground = new InsetDrawable(iconDrawable, inset);
Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor),
foreground);
return mIconFactory.createBadgedIconBitmap(drawable).icon;
}
private void onBubbleBarBoundsChanged() {
int newTop = mBarView.getRestingTopPositionOnScreen();
if (newTop != mLastSentBubbleBarTop) {
@@ -629,20 +663,9 @@ public class BubbleBarController extends IBubblesListener.Stub {
}
}
private void addBubbleInternally(BubbleBarBubble bubble, boolean isExpanding,
boolean suppressAnimation) {
mBubbles.put(bubble.getKey(), bubble);
mBubbleBarViewController.addBubble(bubble, isExpanding,
suppressAnimation, /* bubbleToSelect = */ null);
}
/** Listener of {@link BubbleBarLocation} updates. */
public interface BubbleBarLocationListener {
/** Called when {@link BubbleBarLocation} is animated, but change is not yet final. */
void onBubbleBarLocationAnimated(BubbleBarLocation location);
/** Called when {@link BubbleBarLocation} is updated permanently. */
void onBubbleBarLocationUpdated(BubbleBarLocation location);
/** Interface for checking whether the IME is visible. */
public interface ImeVisibilityChecker {
/** Whether the IME is visible. */
boolean isImeVisible();
}
}
@@ -17,11 +17,10 @@ package com.android.launcher3.taskbar.bubbles
import android.graphics.Bitmap
import android.graphics.Path
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage
import com.android.wm.shell.shared.bubbles.BubbleInfo
import com.android.wm.shell.common.bubbles.BubbleInfo
/** An entity in the bubble bar. */
sealed class BubbleBarItem(open val key: String, open var view: BubbleView)
sealed class BubbleBarItem(open var key: String, open var view: BubbleView)
/** Contains state info about a bubble in the bubble bar as well as presentation information. */
data class BubbleBarBubble(
@@ -31,13 +30,8 @@ data class BubbleBarBubble(
var icon: Bitmap,
var dotColor: Int,
var dotPath: Path,
var appName: String,
var flyoutMessage: BubbleBarFlyoutMessage?,
var appName: String
) : BubbleBarItem(info.key, view)
/** Represents the overflow bubble in the bubble bar. */
data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem(KEY, view) {
companion object {
const val KEY = "Overflow"
}
}
data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view)
@@ -27,10 +27,8 @@ import android.view.View
import android.widget.FrameLayout
import androidx.core.view.updateLayoutParams
import com.android.launcher3.R
import com.android.launcher3.taskbar.bubbles.BubbleBarController.BubbleBarLocationListener
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
import com.android.wm.shell.shared.bubbles.BaseBubblePinController
import com.android.wm.shell.shared.bubbles.BubbleBarLocation
import com.android.wm.shell.common.bubbles.BaseBubblePinController
import com.android.wm.shell.common.bubbles.BubbleBarLocation
/**
* Controller to manage pinning bubble bar to left or right when dragging starts from the bubble bar
@@ -43,17 +41,12 @@ class BubbleBarPinController(
private lateinit var bubbleBarViewController: BubbleBarViewController
private lateinit var bubbleStashController: BubbleStashController
private lateinit var bubbleBarLocationListener: BubbleBarLocationListener
private var exclRectWidth: Float = 0f
private var exclRectHeight: Float = 0f
private var dropTargetView: View? = null
fun init(
bubbleControllers: BubbleControllers,
bubbleBarLocationListener: BubbleBarLocationListener
) {
this.bubbleBarLocationListener = bubbleBarLocationListener
fun init(bubbleControllers: BubbleControllers) {
bubbleBarViewController = bubbleControllers.bubbleBarViewController
bubbleStashController = bubbleControllers.bubbleStashController
exclRectWidth = context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_width)
@@ -92,7 +85,6 @@ class BubbleBarPinController(
val bounds = bubbleBarViewController.bubbleBarBounds
val horizontalMargin = bubbleBarViewController.horizontalMargin
bubbleBarLocationListener.onBubbleBarLocationAnimated(location)
dropTargetView?.updateLayoutParams<FrameLayout.LayoutParams> {
width = bounds.width()
height = bounds.height()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -15,35 +15,22 @@
*/
package com.android.launcher3.taskbar.bubbles;
import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_BUBBLE_BAR;
import android.graphics.Rect;
import android.view.View;
import com.android.launcher3.taskbar.TaskbarControllers;
import com.android.launcher3.taskbar.TaskbarSharedState;
import com.android.launcher3.taskbar.bubbles.BubbleBarViewController.TaskbarViewPropertiesProvider;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleBarLocationOnDemandListener;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.RunnableList;
import java.io.PrintWriter;
import java.util.Optional;
/** Hosts various bubble controllers to facilitate passing between one another. */
/**
* Hosts various bubble controllers to facilitate passing between one another.
*/
public class BubbleControllers {
public final BubbleBarController bubbleBarController;
public final BubbleBarViewController bubbleBarViewController;
public final BubbleStashController bubbleStashController;
public final Optional<BubbleStashedHandleViewController> bubbleStashedHandleViewController;
public final BubbleStashedHandleViewController bubbleStashedHandleViewController;
public final BubbleDragController bubbleDragController;
public final BubbleDismissController bubbleDismissController;
public final BubbleBarPinController bubbleBarPinController;
public final BubblePinController bubblePinController;
public final Optional<BubbleBarSwipeController> bubbleBarSwipeController;
public final BubbleCreator bubbleCreator;
private final RunnableList mPostInitRunnables = new RunnableList();
@@ -56,13 +43,11 @@ public class BubbleControllers {
BubbleBarController bubbleBarController,
BubbleBarViewController bubbleBarViewController,
BubbleStashController bubbleStashController,
Optional<BubbleStashedHandleViewController> bubbleStashedHandleViewController,
BubbleStashedHandleViewController bubbleStashedHandleViewController,
BubbleDragController bubbleDragController,
BubbleDismissController bubbleDismissController,
BubbleBarPinController bubbleBarPinController,
BubblePinController bubblePinController,
Optional<BubbleBarSwipeController> bubbleBarSwipeController,
BubbleCreator bubbleCreator) {
BubblePinController bubblePinController) {
this.bubbleBarController = bubbleBarController;
this.bubbleBarViewController = bubbleBarViewController;
this.bubbleStashController = bubbleStashController;
@@ -71,8 +56,6 @@ public class BubbleControllers {
this.bubbleDismissController = bubbleDismissController;
this.bubbleBarPinController = bubbleBarPinController;
this.bubblePinController = bubblePinController;
this.bubbleBarSwipeController = bubbleBarSwipeController;
this.bubbleCreator = bubbleCreator;
}
/**
@@ -82,44 +65,16 @@ public class BubbleControllers {
* were created
* in constructors for now, as some controllers may still be waiting for init().
*/
public void init(TaskbarSharedState taskbarSharedState, TaskbarControllers taskbarControllers) {
BubbleBarLocationCompositeListener bubbleBarLocationListeners =
new BubbleBarLocationCompositeListener(
taskbarControllers.navbarButtonsViewController,
taskbarControllers.taskbarViewController,
new BubbleBarLocationOnDemandListener(() -> taskbarControllers.uiController)
);
public void init(TaskbarControllers taskbarControllers) {
bubbleBarController.init(this,
bubbleBarLocationListeners,
taskbarSharedState);
bubbleStashedHandleViewController.ifPresent(
controller -> controller.init(/* bubbleControllers = */ this));
bubbleStashController.init(
taskbarControllers.taskbarInsetsController,
bubbleBarViewController,
bubbleStashedHandleViewController.orElse(null),
taskbarControllers::runAfterInit
);
bubbleBarViewController.init(taskbarControllers, /* bubbleControllers = */ this,
new TaskbarViewPropertiesProvider() {
@Override
public Rect getTaskbarViewBounds() {
return taskbarControllers.taskbarViewController
.getTransientTaskbarIconLayoutBoundsInParent();
}
@Override
public MultiPropertyFactory<View>.MultiProperty getIconsAlpha() {
return taskbarControllers.taskbarViewController
.getTaskbarIconAlpha()
.get(ALPHA_INDEX_BUBBLE_BAR);
}
});
taskbarControllers.navbarButtonsViewController::isImeVisible);
bubbleBarViewController.init(taskbarControllers, this);
bubbleStashedHandleViewController.init(taskbarControllers, this);
bubbleStashController.init(taskbarControllers, this);
bubbleDragController.init(/* bubbleControllers = */ this);
bubbleDismissController.init(/* bubbleControllers = */ this);
bubbleBarPinController.init(this, bubbleBarLocationListeners);
bubbleBarPinController.init(this);
bubblePinController.init(this);
bubbleBarSwipeController.ifPresent(c -> c.init(this));
mPostInitRunnables.executeAllAndDestroy();
}
@@ -141,13 +96,7 @@ public class BubbleControllers {
* Cleans up all controllers.
*/
public void onDestroy() {
bubbleStashedHandleViewController.ifPresent(BubbleStashedHandleViewController::onDestroy);
bubbleStashedHandleViewController.onDestroy();
bubbleBarController.onDestroy();
bubbleBarViewController.onDestroy();
}
/** Dumps bubble controllers state. */
public void dump(PrintWriter pw) {
bubbleBarViewController.dump(pw);
}
}
@@ -29,8 +29,8 @@ import androidx.dynamicanimation.animation.DynamicAnimation;
import com.android.launcher3.R;
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.taskbar.TaskbarDragLayer;
import com.android.wm.shell.shared.bubbles.DismissView;
import com.android.wm.shell.shared.magnetictarget.MagnetizedObject;
import com.android.wm.shell.common.bubbles.DismissView;
import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
/**
@@ -144,10 +144,10 @@ public class BubbleDismissController {
if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
if (bubbleView.getBubble() != null) {
mBubbleBarViewController.notifySysUiBubbleDismissed(bubbleView.getBubble());
mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
}
} else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
mBubbleBarViewController.onDismissAllBubbles();
mBubbleBarViewController.onDismissAllBubblesWhileDragging();
}
}
@@ -18,8 +18,7 @@
package com.android.launcher3.taskbar.bubbles
import com.android.launcher3.R
import com.android.wm.shell.R as SharedR
import com.android.wm.shell.shared.bubbles.DismissView
import com.android.wm.shell.common.bubbles.DismissView
/**
* Dismiss view is shared from WMShell. It requires setup with local resources.
@@ -36,9 +35,9 @@ fun DismissView.setup() {
iconSizeResId = R.dimen.bubblebar_dismiss_target_icon_size,
bottomMarginResId = R.dimen.bubblebar_dismiss_target_bottom_margin,
floatingGradientHeightResId = R.dimen.bubblebar_dismiss_floating_gradient_height,
floatingGradientColorResId = android.R.color.system_neutral1_900,
backgroundResId = SharedR.drawable.floating_dismiss_background,
iconResId = SharedR.drawable.floating_dismiss_ic_close,
floatingGradientColorResId = R.color.system_neutral1_900,
backgroundResId = R.drawable.bg_bubble_dismiss_circle,
iconResId = R.drawable.ic_bubble_dismiss_white
)
)
}
@@ -29,14 +29,14 @@ import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import com.android.launcher3.R;
import com.android.wm.shell.shared.bubbles.DismissCircleView;
import com.android.wm.shell.shared.bubbles.DismissView;
//import com.android.wm.shell.shared.animation.PhysicsAnimator;
import com.android.wm.shell.common.bubbles.DismissCircleView;
import com.android.wm.shell.common.bubbles.DismissView;
import app.lawnchair.animation.PhysicsAnimator;
/**
* The animator performs the bubble animations while dragging and coordinates bubble and dismiss
* The animator performs the bubble animations while dragging and coordinates
* bubble and dismiss
* view animations when it gets magnetized, released or dismissed.
*/
public class BubbleDragAnimator {
@@ -46,11 +46,11 @@ public class BubbleDragAnimator {
// 400f matches to MEDIUM_LOW spring stiffness
private static final float TRANSLATION_SPRING_STIFFNESS = 400f;
private final PhysicsAnimator.SpringConfig mDefaultConfig =
new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY);
private final PhysicsAnimator.SpringConfig mTranslationConfig =
new PhysicsAnimator.SpringConfig(TRANSLATION_SPRING_STIFFNESS,
DAMPING_RATIO_LOW_BOUNCY);
private final PhysicsAnimator.SpringConfig mDefaultConfig = new PhysicsAnimator.SpringConfig(STIFFNESS_LOW,
DAMPING_RATIO_LOW_BOUNCY);
private final PhysicsAnimator.SpringConfig mTranslationConfig = new PhysicsAnimator.SpringConfig(
TRANSLATION_SPRING_STIFFNESS,
DAMPING_RATIO_LOW_BOUNCY);
@NonNull
private final View mView;
@NonNull
@@ -114,7 +114,8 @@ public class BubbleDragAnimator {
*
* @param restingPosition the position to animate to
* @param velocity the initial velocity to use for the spring animation
* @param endActions gets called when the animation completes or gets cancelled
* @param endActions gets called when the animation completes or gets
* cancelled
*/
public void animateToRestingState(@NonNull PointF restingPosition, @NonNull PointF velocity,
@Nullable Runnable endActions) {
@@ -130,7 +131,7 @@ public class BubbleDragAnimator {
boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
boolean allRelevantPropertyAnimationsEnded) -> {
if (canceled || allRelevantPropertyAnimationsEnded) {
resetAnimatedViews(restingPosition, /* dismissed= */ false);
resetAnimatedViews(restingPosition);
if (endActions != null) {
endActions.run();
}
@@ -140,7 +141,8 @@ public class BubbleDragAnimator {
}
/**
* Animates the dragged view alongside the dismiss view when it gets captured in the dismiss
* Animates the dragged view alongside the dismiss view when it gets captured in
* the dismiss
* target area.
*/
public void animateDismissCaptured() {
@@ -161,7 +163,8 @@ public class BubbleDragAnimator {
}
/**
* Animates the dragged view alongside the dismiss view when it gets released from the dismiss
* Animates the dragged view alongside the dismiss view when it gets released
* from the dismiss
* target area.
*/
public void animateDismissReleased() {
@@ -182,10 +185,13 @@ public class BubbleDragAnimator {
}
/**
* Animates the dragged bubble dismiss when it's released in the dismiss target area.
* Animates the dragged bubble dismiss when it's released in the dismiss target
* area.
*
* @param initialPosition the initial position to move the bubble too after animation finishes
* @param endActions gets called when the animation completes or gets cancelled
* @param initialPosition the initial position to move the bubble too after
* animation finishes
* @param endActions gets called when the animation completes or gets
* cancelled
*/
public void animateDismiss(@NonNull PointF initialPosition, @Nullable Runnable endActions) {
float dismissHeight = mDismissView != null ? mDismissView.getHeight() : 0f;
@@ -199,8 +205,9 @@ public class BubbleDragAnimator {
boolean wasFling, boolean canceled, float finalValue, float finalVelocity,
boolean allRelevantPropertyAnimationsEnded) -> {
if (canceled || allRelevantPropertyAnimationsEnded) {
resetAnimatedViews(initialPosition, /* dismissed= */ true);
if (endActions != null) endActions.run();
resetAnimatedViews(initialPosition);
if (endActions != null)
endActions.run();
}
})
.start();
@@ -210,14 +217,11 @@ public class BubbleDragAnimator {
* Reset the animated views to the initial state
*
* @param initialPosition position of the bubble
* @param dismissed whether the animated view was dismissed
*/
private void resetAnimatedViews(@NonNull PointF initialPosition, boolean dismissed) {
private void resetAnimatedViews(@NonNull PointF initialPosition) {
mView.setScaleX(1f);
mView.setScaleY(1f);
if (!dismissed) {
mView.setAlpha(1f);
}
mView.setAlpha(1f);
mView.setTranslationX(initialPosition.x);
mView.setTranslationY(initialPosition.y);
@@ -16,31 +16,19 @@
package com.android.launcher3.taskbar.bubbles;
import android.annotation.SuppressLint;
import android.graphics.Point;
import android.graphics.PointF;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.wm.shell.shared.bubbles.BaseBubblePinController.LocationChangeListener;
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.bubbles.DeviceConfig;
import com.android.wm.shell.shared.bubbles.DragZone;
import com.android.wm.shell.shared.bubbles.DragZoneFactory;
import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker;
import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker;
import com.android.wm.shell.shared.bubbles.DraggedObject;
import com.android.wm.shell.shared.bubbles.DropTargetManager;
import com.android.wm.shell.shared.bubbles.DropTargetManager.DragZoneChangedListener;
import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
/**
* Controls bubble bar drag interactions.
@@ -88,36 +76,9 @@ public class BubbleDragController {
private BubbleDismissController mBubbleDismissController;
private BubbleBarPinController mBubbleBarPinController;
private BubblePinController mBubblePinController;
private final DropTargetManager mDropTargetManager;
private final DragZoneFactory mDragZoneFactory;
private final BubbleDragZoneChangedListener mBubbleDragZoneChangedListener;
private boolean mIsDragging;
public BubbleDragController(TaskbarActivityContext activity, FrameLayout dropTargetParent) {
public BubbleDragController(TaskbarActivityContext activity) {
mActivity = activity;
WindowManager windowManager =
mActivity.getApplicationContext().getSystemService(WindowManager.class);
DeviceConfig deviceConfig =
DeviceConfig.create(mActivity.getApplicationContext(), windowManager);
SplitScreenModeChecker splitScreenModeChecker = new SplitScreenModeChecker() {
@NonNull
@Override
public SplitScreenMode getSplitScreenMode() {
return SplitScreenMode.NONE;
}
};
DesktopWindowModeChecker desktopWindowModeChecker = new DesktopWindowModeChecker() {
@Override
public boolean isSupported() {
return false;
}
};
mDragZoneFactory = new DragZoneFactory(mActivity.getApplicationContext(), deviceConfig,
splitScreenModeChecker, desktopWindowModeChecker);
mBubbleDragZoneChangedListener = new BubbleDragZoneChangedListener();
mDropTargetManager = new DropTargetManager(mActivity.getApplicationContext(),
dropTargetParent, mBubbleDragZoneChangedListener);
}
/**
@@ -167,90 +128,45 @@ public class BubbleDragController {
}
};
private BubbleBarLocation getBubbleBarLocationDuringDrag() {
return BubbleAnythingFlagHelper.enableBubbleToFullscreen()
? mBubbleDragZoneChangedListener.mBubbleBarLocation
: mReleasedLocation;
}
@Override
void onDragStart() {
mBubblePinController.setListener(mLocationChangeListener);
mBubbleBarViewController.onBubbleDragStart(bubbleView);
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
DraggedObject.Bubble draggedBubble =
new DraggedObject.Bubble(
mBubbleBarViewController.getBubbleBarLocation());
mDropTargetManager.onDragStarted(draggedBubble,
mDragZoneFactory.createSortedDragZones(draggedBubble));
} else {
mBubblePinController.setListener(mLocationChangeListener);
mBubblePinController.onDragStart(
mBubbleBarViewController.getBubbleBarLocation().isOnLeft(
bubbleView.isLayoutRtl()));
}
mBubblePinController.onDragStart(
mBubbleBarViewController.getBubbleBarLocation().isOnLeft(
bubbleView.isLayoutRtl()));
}
@Override
protected void onDragUpdate(float x, float y, float newTx, float newTy) {
bubbleView.setDragTranslationX(newTx);
bubbleView.setTranslationY(newTy);
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
mDropTargetManager.onDragUpdated((int) x, (int) y);
} else {
mBubblePinController.onDragUpdate(x, y);
}
mBubblePinController.onDragUpdate(x, y);
}
@Override
protected void onDragRelease() {
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
mDropTargetManager.onDragEnded();
if (!mBubbleDragZoneChangedListener.isDraggedToFullscreen()) {
// TODO b/393173014: check for desktop window and split once they're
// implemented. this notifies wm shell that the dragged bubble was
// released so that we can show the expanded view. we only want to show it
// after releasing in a Bubble zone. But Split and Desktop Window aren't
// implemented yet, so we only check for full screen for now.
mBubbleBarViewController.onBubbleDragRelease(
getBubbleBarLocationDuringDrag());
}
} else {
mBubblePinController.onDragEnd();
mBubbleBarViewController.onBubbleDragRelease(getBubbleBarLocationDuringDrag());
}
mBubblePinController.onDragEnd();
mBubbleBarViewController.onBubbleDragRelease(mReleasedLocation);
}
@Override
protected void onDragDismiss() {
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
mDropTargetManager.onDragEnded();
} else {
mBubblePinController.onDragEnd();
}
mBubbleBarViewController.onBubbleDismissed(bubbleView);
mBubblePinController.onDragEnd();
mBubbleBarViewController.onBubbleDragEnd();
}
@Override
void onDragEnd(float x, float y) {
mBubbleBarController.updateBubbleBarLocation(getBubbleBarLocationDuringDrag(),
BubbleBarLocation.UpdateSource.DRAG_BUBBLE);
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
mDropTargetManager.onDragEnded();
if (mBubbleDragZoneChangedListener.isDraggedToFullscreen()) {
mBubbleBarViewController.moveDraggedBubbleToFullscreen(
bubbleView, new Point((int) x, (int) y));
}
} else {
mBubblePinController.setListener(null);
}
void onDragEnd() {
mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
mBubbleBarViewController.onBubbleDragEnd();
mBubblePinController.setListener(null);
}
@Override
protected PointF getRestingPosition() {
return mBubbleBarViewController.getDraggedBubbleReleaseTranslation(
getInitialPosition(), getBubbleBarLocationDuringDrag());
getInitialPosition(), mReleasedLocation);
}
});
}
@@ -268,12 +184,6 @@ public class BubbleDragController {
private final LocationChangeListener mLocationChangeListener =
location -> mReleasedLocation = location;
private BubbleBarLocation getBubbleBarLocationDuringDrag() {
return BubbleAnythingFlagHelper.enableBubbleToFullscreen()
? mBubbleDragZoneChangedListener.mBubbleBarLocation
: mReleasedLocation;
}
@Override
protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) {
if (bubbleBarView.isExpanded()) return false;
@@ -282,88 +192,53 @@ public class BubbleDragController {
@Override
void onDragStart() {
mBubbleBarPinController.setListener(mLocationChangeListener);
initialRelativePivot.set(bubbleBarView.getRelativePivotX(),
bubbleBarView.getRelativePivotY());
// 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.setIsDragging(true);
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
DraggedObject.BubbleBar draggedBubbleBar = new DraggedObject.BubbleBar(
mBubbleBarViewController.getBubbleBarLocation());
mDropTargetManager.onDragStarted(draggedBubbleBar,
mDragZoneFactory.createSortedDragZones(draggedBubbleBar));
} else {
mBubbleBarPinController.setListener(mLocationChangeListener);
mBubbleBarPinController.onDragStart(
bubbleBarView.getBubbleBarLocation().isOnLeft(
bubbleBarView.isLayoutRtl()));
}
mBubbleBarPinController.onDragStart(
bubbleBarView.getBubbleBarLocation().isOnLeft(bubbleBarView.isLayoutRtl()));
}
@Override
protected void onDragUpdate(float x, float y, float newTx, float newTy) {
bubbleBarView.setTranslationX(newTx);
bubbleBarView.setTranslationY(newTy);
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
mDropTargetManager.onDragUpdated((int) x, (int) y);
} else {
mBubbleBarPinController.onDragUpdate(x, y);
}
mBubbleBarPinController.onDragUpdate(x, y);
}
@Override
protected void onDragRelease() {
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
mDropTargetManager.onDragEnded();
} else {
mBubbleBarPinController.onDragEnd();
}
mBubbleBarPinController.onDragEnd();
}
@Override
protected void onDragDismiss() {
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
mDropTargetManager.onDragEnded();
} else {
mBubbleBarPinController.onDragEnd();
}
mBubbleBarPinController.onDragEnd();
}
@Override
void onDragEnd(float x, float y) {
void onDragEnd() {
// Make sure to update location as the first thing. Pivot update causes a relayout
mBubbleBarController.updateBubbleBarLocation(getBubbleBarLocationDuringDrag(),
BubbleBarLocation.UpdateSource.DRAG_BAR);
mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
bubbleBarView.setIsDragging(false);
// Restoring the initial pivot for the bubble bar view
bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
mBubbleBarViewController.onBubbleBarDragEnd();
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
mDropTargetManager.onDragEnded();
} else {
mBubbleBarPinController.setListener(null);
}
mBubbleBarPinController.setListener(null);
}
@Override
protected PointF getRestingPosition() {
return mBubbleBarViewController.getBubbleBarDragReleaseTranslation(
getInitialPosition(), getBubbleBarLocationDuringDrag());
getInitialPosition(), mReleasedLocation);
}
});
}
/** Whether there is an item being dragged or not. */
public boolean isDragging() {
return mIsDragging;
}
/** Sets whether something is being dragged or not. */
public void setIsDragging(boolean isDragging) {
mIsDragging = isDragging;
}
/**
* Bubble touch listener for handling a single bubble view or bubble bar view while dragging.
* The dragging starts after "shorter" long click (the long click duration might change):
@@ -409,7 +284,7 @@ public class BubbleDragController {
private final PointF mTouchDownLocation = new PointF();
private final PointF mViewInitialPosition = new PointF();
private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout();
private final long mPressToDragTimeout = ViewConfiguration.getLongPressTimeout() / 2;
private State mState = State.IDLE;
private int mTouchSlop = -1;
private BubbleDragAnimator mAnimator;
@@ -430,7 +305,7 @@ public class BubbleDragController {
/**
* Called when the dragging interaction has ended and all the animations have completed
*/
abstract void onDragEnd(float x, float y);
abstract void onDragEnd();
/**
* Called when the dragged bubble is released outside of the dismiss target area and will
@@ -560,7 +435,6 @@ public class BubbleDragController {
private void startDragging(@NonNull View view) {
onDragStart();
BubbleDragController.this.setIsDragging(true);
mActivity.setTaskbarWindowFullscreen(true);
mAnimator = new BubbleDragAnimator(view);
mAnimator.animateFocused();
@@ -577,11 +451,10 @@ public class BubbleDragController {
}
private void stopDragging(@NonNull View view, @NonNull MotionEvent event) {
BubbleDragController.this.setIsDragging(false);
Runnable onComplete = () -> {
mActivity.setTaskbarWindowFullscreen(false);
cleanUp(view);
onDragEnd(event.getRawX(), event.getRawY());
onDragEnd();
};
if (mBubbleDismissController.handleTouchEvent(event)) {
@@ -589,17 +462,8 @@ public class BubbleDragController {
mAnimator.animateDismiss(mViewInitialPosition, onComplete);
} else {
onDragRelease();
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
if (mBubbleDragZoneChangedListener.isDraggedToFullscreen()) {
onComplete.run();
} else {
mAnimator.animateToRestingState(getRestingPosition(), getCurrentVelocity(),
onComplete);
}
} else {
mAnimator.animateToRestingState(getRestingPosition(), getCurrentVelocity(),
mAnimator.animateToRestingState(getRestingPosition(), getCurrentVelocity(),
onComplete);
}
}
mBubbleDismissController.hideDismissView();
}
@@ -639,46 +503,4 @@ public class BubbleDragController {
return new PointF(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
}
}
private class BubbleDragZoneChangedListener implements DragZoneChangedListener {
private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT;
private DragZone mDragZone;
boolean isDraggedToFullscreen() {
return mDragZone instanceof DragZone.FullScreen;
}
@Override
public void onInitialDragZoneSet(@NonNull DragZone dragZone) {
mDragZone = dragZone;
if (dragZone instanceof DragZone.Bubble.Left) {
mBubbleBarLocation = BubbleBarLocation.LEFT;
} else if (dragZone instanceof DragZone.Bubble.Right) {
mBubbleBarLocation = BubbleBarLocation.RIGHT;
}
}
@Override
public void onDragZoneChanged(@NonNull DraggedObject draggedObject, @NonNull DragZone from,
@NonNull DragZone to) {
mDragZone = to;
if (to instanceof DragZone.Bubble.Left
&& mBubbleBarLocation != BubbleBarLocation.LEFT) {
if (draggedObject instanceof DraggedObject.Bubble) {
mBubbleBarController.animateBubbleBarLocation(BubbleBarLocation.LEFT);
}
mBubbleBarLocation = BubbleBarLocation.LEFT;
} else if (to instanceof DragZone.Bubble.Right
&& mBubbleBarLocation != BubbleBarLocation.RIGHT) {
if (draggedObject instanceof DraggedObject.Bubble) {
mBubbleBarController.animateBubbleBarLocation(BubbleBarLocation.RIGHT);
}
mBubbleBarLocation = BubbleBarLocation.RIGHT;
}
}
@Override
public void onDragEnded(@NonNull DragZone zone) {}
}
}
@@ -27,9 +27,8 @@ import android.view.View
import android.widget.FrameLayout
import androidx.core.view.updateLayoutParams
import com.android.launcher3.R
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
import com.android.wm.shell.shared.bubbles.BaseBubblePinController
import com.android.wm.shell.shared.bubbles.BubbleBarLocation
import com.android.wm.shell.common.bubbles.BaseBubblePinController
import com.android.wm.shell.common.bubbles.BubbleBarLocation
/** Controller to manage pinning bubble bar to left or right when dragging starts from a bubble */
class BubblePinController(
@@ -21,7 +21,6 @@ import static android.view.View.VISIBLE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.content.res.Resources;
import android.graphics.Outline;
import android.graphics.Rect;
@@ -29,51 +28,47 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewOutlineProvider;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.anim.RevealOutlineAnimation;
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
import com.android.launcher3.taskbar.StashedHandleView;
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
import com.android.launcher3.taskbar.TaskbarControllers;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.animation.PhysicsAnimator;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.handles.RegionSamplingHelper;
/**
* Handles properties/data collection, then passes the results to our stashed handle View to render.
* Handles properties/data collection, then passes the results to our stashed
* handle View to render.
*/
public class BubbleStashedHandleViewController {
private final TaskbarActivityContext mActivity;
private final StashedHandleView mStashedHandleView;
private final MultiValueAlpha mStashedHandleAlpha;
private float mTranslationForSwipeY;
private float mTranslationForStashY;
// Initialized in init.
private BubbleBarViewController mBarViewController;
private BubbleStashController mBubbleStashController;
private RegionSamplingHelper mRegionSamplingHelper;
// Height of the area for the stash handle. Handle will be drawn in the center of this.
// This is also the area where touch is handled on the handle.
private int mStashedBubbleBarHeight;
private int mBarSize;
private int mStashedTaskbarHeight;
private int mStashedHandleWidth;
private int mStashedHandleHeight;
// The bounds of the stashed handle in settled state.
// The bounds we want to clip to in the settled state when showing the stashed
// handle.
private final Rect mStashedHandleBounds = new Rect();
private float mStashedHandleRadius;
// When the reveal animation is cancelled, we can assume it's about to create a new animation,
// When the reveal animation is cancelled, we can assume it's about to create a
// new animation,
// which should start off at the same point the cancelled one left off.
private float mStartProgressForNextRevealAnim;
// Use a nullable boolean to handle initial case where the last animation direction is not known
@Nullable
private Boolean mWasLastRevealAnimReversed = null;
private boolean mWasLastRevealAnimReversed;
// XXX: if there are more of these maybe do state flags instead
private boolean mHiddenForSysui;
@@ -85,39 +80,32 @@ public class BubbleStashedHandleViewController {
mActivity = activity;
mStashedHandleView = stashedHandleView;
mStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, 1);
mStashedHandleAlpha.setUpdateVisibility(true);
}
/** Initialize controller. */
public void init(BubbleControllers bubbleControllers) {
public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
mBarViewController = bubbleControllers.bubbleBarViewController;
mBubbleStashController = bubbleControllers.bubbleStashController;
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
Resources resources = mActivity.getResources();
mStashedHandleHeight = resources.getDimensionPixelSize(
R.dimen.bubblebar_stashed_handle_height);
mStashedHandleWidth = resources.getDimensionPixelSize(
R.dimen.bubblebar_stashed_handle_width);
mBarSize = resources.getDimensionPixelSize(R.dimen.bubblebar_size);
int barSize = resources.getDimensionPixelSize(R.dimen.bubblebar_size);
// Use the max translation for bubble bar whether it is on the home screen or in app.
// Use values directly from device profile to avoid referencing other bubble controllers
// during init flow.
int maxTy = Math.max(deviceProfile.hotseatBarBottomSpacePx,
deviceProfile.taskbarBottomMargin);
// Adjust handle view size to accommodate the handle morphing into the bubble bar
mStashedHandleView.getLayoutParams().height = barSize + maxTy;
final int bottomMargin = resources.getDimensionPixelSize(
R.dimen.transient_taskbar_bottom_margin);
mStashedHandleView.getLayoutParams().height = mBarSize + bottomMargin;
mStashedHandleAlpha.get(0).setValue(0);
mStashedBubbleBarHeight = resources.getDimensionPixelSize(
mStashedTaskbarHeight = resources.getDimensionPixelSize(
R.dimen.bubblebar_stashed_size);
mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
mStashedHandleRadius = view.getHeight() / 2f;
outline.setRoundRect(mStashedHandleBounds, mStashedHandleRadius);
float stashedHandleRadius = view.getHeight() / 2f;
outline.setRoundRect(mStashedHandleBounds, stashedHandleRadius);
}
});
@@ -132,10 +120,10 @@ public class BubbleStashedHandleViewController {
public Rect getSampledRegion(View sampledView) {
return mStashedHandleView.getSampledRegion();
}
}, Executors.MAIN_EXECUTOR, Executors.UI_HELPER_EXECUTOR);
}, Executors.UI_HELPER_EXECUTOR);
mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
updateBounds(mBarViewController.getBubbleBarLocation()));
mStashedHandleView.addOnLayoutChangeListener(
(view, i, i1, i2, i3, i4, i5, i6, i7) -> updateBounds(mBarViewController.getBubbleBarLocation()));
}
/** Returns the [PhysicsAnimator] for the stashed handle view. */
@@ -144,27 +132,30 @@ public class BubbleStashedHandleViewController {
}
private void updateBounds(BubbleBarLocation bubbleBarLocation) {
// As more bubbles get added, the icon bounds become larger. To ensure a consistent
// As more bubbles get added, the icon bounds become larger. To ensure a
// consistent
// handle bar position, we pin it to the edge of the screen.
final int stashedCenterY = mStashedHandleView.getHeight() - mStashedBubbleBarHeight / 2;
final int stashedCenterX;
final int stashedCenterY = mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2;
if (bubbleBarLocation.isOnLeft(mStashedHandleView.isLayoutRtl())) {
final int left = mBarViewController.getHorizontalMargin();
stashedCenterX = left + mStashedHandleWidth / 2;
mStashedHandleBounds.set(
left,
stashedCenterY - mStashedHandleHeight / 2,
left + mStashedHandleWidth,
stashedCenterY + mStashedHandleHeight / 2);
mStashedHandleView.setPivotX(0);
} else {
final int right =
mStashedHandleView.getRight() - mBarViewController.getHorizontalMargin();
stashedCenterX = right - mStashedHandleWidth / 2;
final int right = mActivity.getDeviceProfile().widthPx - mBarViewController.getHorizontalMargin();
mStashedHandleBounds.set(
right - mStashedHandleWidth,
stashedCenterY - mStashedHandleHeight / 2,
right,
stashedCenterY + mStashedHandleHeight / 2);
mStashedHandleView.setPivotX(mStashedHandleView.getWidth());
}
mStashedHandleBounds.set(
stashedCenterX - mStashedHandleWidth / 2,
stashedCenterY - mStashedHandleHeight / 2,
stashedCenterX + mStashedHandleWidth / 2,
stashedCenterY + mStashedHandleHeight / 2
);
mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
mStashedHandleView.setPivotX(stashedCenterX);
mStashedHandleView.setPivotY(stashedCenterY);
mStashedHandleView.setPivotY(mStashedHandleView.getHeight() - mStashedTaskbarHeight / 2f);
}
public void onDestroy() {
@@ -172,13 +163,6 @@ public class BubbleStashedHandleViewController {
mRegionSamplingHelper = null;
}
/**
* Returns the width of the stashed handle.
*/
public int getStashedWidth() {
return mStashedHandleWidth;
}
/**
* Returns the height of the stashed handle.
*/
@@ -187,14 +171,16 @@ public class BubbleStashedHandleViewController {
}
/**
* Returns bounds of the stashed handle view
* Returns the height when the bubble bar is unstashed (so the height of the
* bubble bar).
*/
public void getBounds(Rect bounds) {
bounds.set(mStashedHandleBounds);
public int getUnstashedHeight() {
return mBarSize;
}
/**
* Called when system ui state changes. Bubbles don't show when the device is locked.
* Called when system ui state changes. Bubbles don't show when the device is
* locked.
*/
public void setHiddenForSysui(boolean hidden) {
if (mHiddenForSysui != hidden) {
@@ -204,7 +190,8 @@ public class BubbleStashedHandleViewController {
}
/**
* Called when the handle should be hidden (or shown) because there are no bubbles
* Called when the handle should be hidden (or shown) because there are no
* bubbles
* (or 1+ bubbles).
*/
public void setHiddenForBubbles(boolean hidden) {
@@ -215,7 +202,8 @@ public class BubbleStashedHandleViewController {
}
/**
* Called when the home button is enabled / disabled. Bubbles don't show if home is disabled.
* Called when the home button is enabled / disabled. Bubbles don't show if home
* is disabled.
*/
// TODO: is this needed for bubbles?
public void setIsHomeButtonDisabled(boolean homeDisabled) {
@@ -235,7 +223,8 @@ public class BubbleStashedHandleViewController {
}
/**
* Called when bubble bar is stash state changes so that updates to the stashed handle color
* Called when bubble bar is stash state changes so that updates to the stashed
* handle color
* can be started or stopped.
*/
public void onIsStashedChanged() {
@@ -260,56 +249,37 @@ public class BubbleStashedHandleViewController {
* Sets the translation of the stashed handle during the swipe up gesture.
*/
public void setTranslationYForSwipe(float transY) {
mTranslationForSwipeY = transY;
updateTranslationY();
mStashedHandleView.setTranslationY(transY);
}
/**
* Sets the translation of the stashed handle during the spring on stash animation.
*/
public void setTranslationYForStash(float transY) {
mTranslationForStashY = transY;
updateTranslationY();
}
/** Sets translation X for stash handle. */
public void setTranslationX(float translationX) {
mStashedHandleView.setTranslationX(translationX);
}
private void updateTranslationY() {
mStashedHandleView.setTranslationY(mTranslationForSwipeY + mTranslationForStashY);
}
/** Returns the translation of the stashed handle. */
public float getTranslationY() {
return mStashedHandleView.getTranslationY();
}
/**
* Used by {@link BubbleStashController} to animate the handle when stashing or un stashing.
* Used by {@link BubbleStashController} to animate the handle when stashing or
* un stashing.
*/
public MultiPropertyFactory<View> getStashedHandleAlpha() {
return mStashedHandleAlpha;
}
/**
* Creates and returns an Animator that updates the stashed handle shape and size.
* When stashed, the shape is a thin rounded pill. When unstashed, the shape morphs into
* Creates and returns an Animator that updates the stashed handle shape and
* size.
* When stashed, the shape is a thin rounded pill. When unstashed, the shape
* morphs into
* the size of where the bubble bar icons will be.
*/
public Animator createRevealAnimToIsStashed(boolean isStashed) {
Rect bubbleBarBounds = getLocalBubbleBarBounds();
Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds());
float bubbleBarRadius = bubbleBarBounds.height() / 2f;
// Account for the full visual height of the bubble bar
int heightDiff = (mBarSize - bubbleBarBounds.height()) / 2;
bubbleBarBounds.top -= heightDiff;
bubbleBarBounds.bottom += heightDiff;
float stashedHandleRadius = mStashedHandleView.getHeight() / 2f;
final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
bubbleBarRadius, mStashedHandleRadius, bubbleBarBounds, mStashedHandleBounds);
stashedHandleRadius, stashedHandleRadius, bubbleBarBounds, mStashedHandleBounds);
boolean isReversed = !isStashed;
// We are only changing direction when mWasLastRevealAnimReversed is set at least once
boolean changingDirection =
mWasLastRevealAnimReversed != null && mWasLastRevealAnimReversed != isReversed;
boolean changingDirection = mWasLastRevealAnimReversed != isReversed;
mWasLastRevealAnimReversed = isReversed;
if (changingDirection) {
mStartProgressForNextRevealAnim = 1f - mStartProgressForNextRevealAnim;
@@ -327,32 +297,25 @@ public class BubbleStashedHandleViewController {
}
/**
* Get bounds for the bubble bar in the space of the handle view
* Checks that the stash handle is visible and that the motion event is within
* bounds.
*/
private Rect getLocalBubbleBarBounds() {
// Position the bubble bar bounds to the space of handle view
Rect bubbleBarBounds = new Rect(mBarViewController.getBubbleBarBounds());
// Start by moving bubble bar bounds to the bottom of handle view
int height = bubbleBarBounds.height();
bubbleBarBounds.bottom = mStashedHandleView.getHeight();
bubbleBarBounds.top = bubbleBarBounds.bottom - height;
// Then apply translation that is applied to the bubble bar
bubbleBarBounds.offset(0, (int) mBubbleStashController.getBubbleBarTranslationY());
return bubbleBarBounds;
}
/** Checks that the stash handle is visible and that the motion event is within bounds. */
public boolean isEventOverHandle(MotionEvent ev) {
if (mStashedHandleView.getVisibility() != VISIBLE) {
return false;
}
// the bounds of the handle only include the visible part, so we check that the Y coordinate
// is anywhere within the stashed height of bubble bar (same as taskbar stashed height).
final int top = mActivity.getDeviceProfile().heightPx - mStashedBubbleBarHeight;
final float x = ev.getRawX();
return ev.getRawY() >= top && x >= mStashedHandleBounds.left
&& x <= mStashedHandleBounds.right;
// the bounds of the handle only include the visible part, so we check that the
// Y coordinate
// is anywhere within the stashed taskbar height.
int top = mActivity.getDeviceProfile().heightPx - mStashedTaskbarHeight;
return (int) ev.getRawY() >= top && containsX((int) ev.getRawX());
}
/** Checks if the given x coordinate is within the stashed handle bounds. */
public boolean containsX(int x) {
return x >= mStashedHandleBounds.left && x <= mStashedHandleBounds.right;
}
/** Set a bubble bar location */
@@ -16,30 +16,24 @@
package com.android.launcher3.taskbar.bubbles;
import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.widget.ImageView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.launcher3.R;
import com.android.launcher3.icons.DotRenderer;
import com.android.wm.shell.shared.animation.Interpolators;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.bubbles.BubbleInfo;
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.animation.Interpolators;
import com.patrykmichalik.opto.core.PreferenceExtensionsKt;
import app.lawnchair.preferences2.PreferenceManager2;
import app.lawnchair.theme.color.ColorOption;
@@ -49,18 +43,34 @@ import java.util.EnumSet;
// TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
/**
* View that displays a bubble icon, along with an app badge on either the left or
* View that displays a bubble icon, along with an app badge on either the left
* or
* right side of the view.
*/
public class BubbleView extends ConstraintLayout {
public static final int DEFAULT_PATH_SIZE = 100;
/** Duration for animating the scale of the dot and badge. */
private static final int SCALE_ANIMATION_DURATION_MS = 200;
/**
* Flags that suppress the visibility of the 'new' dot or the app badge, for one
* reason or
* another. If any of these flags are set, the dot will not be shown.
* If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
*/
enum SuppressionFlag {
// TODO: (b/277815200) implement flyout
// Suppressed because the flyout is visible - it will morph into the dot via
// animation.
FLYOUT_VISIBLE,
// Suppressed because this bubble is behind others in the collapsed stack.
BEHIND_STACK,
}
private final EnumSet<SuppressionFlag> mSuppressionFlags = EnumSet.noneOf(SuppressionFlag.class);
private final ImageView mBubbleIcon;
private final ImageView mAppIcon;
private int mBubbleSize;
private final int mBubbleSize;
private float mDragTranslationX;
private float mOffsetX;
@@ -76,22 +86,12 @@ public class BubbleView extends ConstraintLayout {
private float mAnimatingToDotScale;
// The current scale value of the dot
private float mDotScale;
private boolean mDotSuppressedForBubbleUpdate = false;
// TODO: (b/273310265) handle RTL
// Whether the bubbles are positioned on the left or right side of the screen
private boolean mOnLeft = false;
private BubbleBarItem mBubble;
private boolean mIsOverflow;
private Bitmap mIcon;
@Nullable
private Controller mController;
@Nullable
private BubbleBarBubbleIconsFactory mIconFactory = null;
PreferenceManager2 preferenceManager2;
@@ -114,6 +114,8 @@ public class BubbleView extends ConstraintLayout {
setLayoutDirection(LAYOUT_DIRECTION_LTR);
LayoutInflater.from(context).inflate(R.layout.bubble_view, this);
mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
mBubbleIcon = findViewById(R.id.icon_view);
mAppIcon = findViewById(R.id.app_icon_view);
@@ -123,26 +125,24 @@ public class BubbleView extends ConstraintLayout {
setFocusable(true);
setClickable(true);
// We manage the shadow ourselves when creating the bitmap
setOutlineAmbientShadowColor(Color.TRANSPARENT);
setOutlineSpotShadowColor(Color.TRANSPARENT);
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
BubbleView.this.getOutline(outline);
}
});
}
private void updateBubbleSizeAndDotRender() {
int updatedBubbleSize = Math.min(getWidth(), getHeight());
if (updatedBubbleSize == mBubbleSize) return;
mBubbleSize = updatedBubbleSize;
mIconFactory = new BubbleBarBubbleIconsFactory(mContext, mBubbleSize);
updateBubbleIcon();
if (mBubble == null || mBubble instanceof BubbleBarOverflow) return;
Path dotPath = ((BubbleBarBubble) mBubble).getDotPath();
mDotRenderer = new DotRenderer(mBubbleSize, dotPath, DEFAULT_PATH_SIZE);
private void getOutline(Outline outline) {
final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
final int inset = (mBubbleSize - normalizedSize) / 2;
outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
}
/**
* Set translation-x while this bubble is being dragged.
* Translation applied to the view is a sum of {@code translationX} and offset defined by
* Translation applied to the view is a sum of {@code translationX} and offset
* defined by
* {@link #setOffsetX(float)}.
*/
public void setDragTranslationX(float translationX) {
@@ -159,9 +159,11 @@ public class BubbleView extends ConstraintLayout {
/**
* Set offset on x-axis while dragging.
* Used to counter parent translation in order to keep the dragged view at the current position
* Used to counter parent translation in order to keep the dragged view at the
* current position
* on screen.
* Translation applied to the view is a sum of {@code offsetX} and translation defined by
* Translation applied to the view is a sum of {@code offsetX} and translation
* defined by
* {@link #setDragTranslationX(float)}
*/
public void setOffsetX(float offsetX) {
@@ -173,12 +175,6 @@ public class BubbleView extends ConstraintLayout {
setTranslationX(mDragTranslationX + mOffsetX);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
updateBubbleSizeAndDotRender();
}
@Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
@@ -189,78 +185,19 @@ public class BubbleView extends ConstraintLayout {
getDrawingRect(mTempBounds);
mDrawParams.dotColor = mDotColor;
mDrawParams.appColor = mDotColor;
mDrawParams.iconBounds = mTempBounds;
mDrawParams.leftAlign = mOnLeft;
mDrawParams.scale = mDotScale;
mDotRenderer.draw(canvas, mDrawParams);
}
@Override
public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfoInternal(info);
info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
if (mBubble instanceof BubbleBarBubble) {
info.addAction(AccessibilityNodeInfo.ACTION_DISMISS);
}
if (mController != null) {
if (mController.getBubbleBarLocation().isOnLeft(isLayoutRtl())) {
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right,
getResources().getString(R.string.bubble_bar_action_move_right)));
} else {
info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left,
getResources().getString(R.string.bubble_bar_action_move_left)));
}
}
}
@Override
public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
if (super.performAccessibilityActionInternal(action, arguments)) {
return true;
}
if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
if (mController != null) {
mController.collapse();
}
return true;
}
if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
if (mController != null) {
mController.dismiss(this);
}
return true;
}
if (action == R.id.action_move_left) {
if (mController != null) {
mController.updateBubbleBarLocation(BubbleBarLocation.LEFT,
BubbleBarLocation.UpdateSource.A11Y_ACTION_BUBBLE);
}
}
if (action == R.id.action_move_right) {
if (mController != null) {
mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT,
BubbleBarLocation.UpdateSource.A11Y_ACTION_BUBBLE);
}
}
return false;
}
void setController(@Nullable Controller controller) {
mController = controller;
mDotRenderer.draw(canvas, mDrawParams, -1);
}
/** Sets the bubble being rendered in this view. */
public void setBubble(BubbleBarBubble bubble) {
mBubble = bubble;
mIcon = bubble.getIcon();
updateBubbleIcon();
if (bubble.getInfo().showAppBadge()) {
mAppIcon.setImageBitmap(bubble.getBadge());
} else {
mAppIcon.setVisibility(GONE);
}
mBubbleIcon.setImageBitmap(bubble.getIcon());
mAppIcon.setImageBitmap(bubble.getBadge());
mDotColor = bubble.getDotColor();
ColorOption dotColorOption = PreferenceExtensionsKt.firstBlocking(preferenceManager2.getNotificationDotColor());
int dotColor = dotColorOption.getColorPreferenceEntry().getLightColor().invoke(getContext());
@@ -281,53 +218,32 @@ public class BubbleView extends ConstraintLayout {
setContentDescription(contentDesc);
}
private void updateBubbleIcon() {
Bitmap icon = null;
if (mIcon != null) {
icon = mIcon;
if (mIconFactory != null) {
BitmapDrawable iconDrawable = new BitmapDrawable(getResources(), icon);
icon = mIconFactory.createShadowedIconBitmap(iconDrawable, /* scale = */ 1f);
}
}
mBubbleIcon.setImageBitmap(icon);
}
/**
* Sets that this bubble represents the overflow. The overflow appears in the list of bubbles
* but does not represent app content, instead it shows recent bubbles that couldn't fit into
* the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't
* Sets that this bubble represents the overflow. The overflow appears in the
* list of bubbles
* but does not represent app content, instead it shows recent bubbles that
* couldn't fit into
* the list of bubbles. It doesn't show an app icon because it is part of system
* UI / doesn't
* come from an app.
*/
public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
mBubble = overflow;
mIsOverflow = true;
mIcon = bitmap;
updateBubbleIcon();
mBubbleIcon.setImageBitmap(bitmap);
mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
setContentDescription(getResources().getString(R.string.bubble_bar_overflow_description));
}
/** Whether this view represents the overflow button. */
public boolean isOverflow() {
return mIsOverflow;
}
/** Returns the bubble being rendered in this view. */
@Nullable
public BubbleBarItem getBubble() {
return mBubble;
}
/** Updates the dot visibility if it's not suppressed based on whether it has unseen content. */
public void updateDotVisibility(boolean animate) {
if (mDotSuppressedForBubbleUpdate) {
// if the dot is suppressed for an update, there's nothing to do
return;
}
final float targetScale = hasUnseenContent() ? 1f : 0f;
void updateDotVisibility(boolean animate) {
final float targetScale = shouldDrawDot() ? 1f : 0f;
if (animate) {
animateDotScale(targetScale);
animateDotScale();
} else {
mDotScale = targetScale;
mAnimatingToDotScale = targetScale;
@@ -335,132 +251,77 @@ public class BubbleView extends ConstraintLayout {
}
}
void setBadgeScale(float fraction) {
if (hasBadge()) {
mAppIcon.setScaleX(fraction);
mAppIcon.setScaleY(fraction);
}
}
void showBadge() {
animateBadgeScale(1);
}
void hideBadge() {
animateBadgeScale(0);
}
private boolean hasBadge() {
return mAppIcon.getVisibility() == VISIBLE;
}
private void animateBadgeScale(float scale) {
if (!hasBadge()) {
void updateBadgeVisibility() {
if (mBubble instanceof BubbleBarOverflow) {
// The overflow bubble does not have a badge, so just bail.
return;
}
mAppIcon.clearAnimation();
mAppIcon.animate()
.setDuration(SCALE_ANIMATION_DURATION_MS)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.scaleX(scale)
.scaleY(scale)
.start();
BubbleBarBubble bubble = (BubbleBarBubble) mBubble;
Bitmap appBadgeBitmap = bubble.getBadge();
int translationX = mOnLeft
? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
: 0;
mAppIcon.setTranslationX(translationX);
mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
}
/** Suppresses drawing the dot due to an update for this bubble. */
public void suppressDotForBubbleUpdate() {
mDotSuppressedForBubbleUpdate = true;
setDotScale(0);
/** Sets whether this bubble is in the stack & not the first bubble. **/
void setBehindStack(boolean behindStack, boolean animate) {
if (behindStack) {
mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK);
} else {
mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
}
updateDotVisibility(animate);
updateBadgeVisibility();
}
/**
* Unsuppresses the dot after the bubble update finished animating.
*
* @param animate whether or not to animate the dot back in
*/
public void unsuppressDotForBubbleUpdate(boolean animate) {
mDotSuppressedForBubbleUpdate = false;
showDotIfNeeded(animate);
/** Whether this bubble is in the stack & not the first bubble. **/
boolean isBehindStack() {
return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
}
boolean hasUnseenContent() {
return mBubble != null
&& mBubble instanceof BubbleBarBubble
&& !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
}
/**
* Used to determine if we can skip drawing frames.
*
* <p>Generally we should draw the dot when it is requested to be shown and there is unseen
* content. But when the dot is removed, we still want to draw frames so that it can be scaled
* out.
*/
/** Whether the dot indicating unseen content in a bubble should be shown. */
private boolean shouldDrawDot() {
// if there's no dot there's nothing to draw, unless the dot was removed and we're in the
// middle of removing it
return hasUnseenContent() || mDotIsAnimating;
boolean bubbleHasUnseenContent = mBubble != null
&& mBubble instanceof BubbleBarBubble
&& mSuppressionFlags.isEmpty()
&& !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
// Always render the dot if it's animating, since it could be animating out.
// Otherwise, show
// it if the bubble wants to show it, and we aren't suppressing it.
return bubbleHasUnseenContent || mDotIsAnimating;
}
/** Updates the dot scale to the specified fraction from 0 to 1. */
/** How big the dot should be, fraction from 0 to 1. */
private void setDotScale(float fraction) {
if (!shouldDrawDot()) {
return;
}
mDotScale = fraction;
invalidate();
}
void showDotIfNeeded(float fraction) {
if (!hasUnseenContent()) {
return;
}
setDotScale(fraction);
}
void showDotIfNeeded(boolean animate) {
// only show the dot if we have unseen content and it's not suppressed
if (!hasUnseenContent() || mDotSuppressedForBubbleUpdate) {
return;
}
if (animate) {
animateDotScale(1f);
} else {
setDotScale(1f);
}
}
void hideDot() {
animateDotScale(0f);
}
/** Marks this bubble such that it no longer has unseen content, and hides the dot. */
void markSeen() {
if (mBubble instanceof BubbleBarBubble bubble) {
BubbleInfo info = bubble.getInfo();
info.setFlags(
info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
hideDot();
}
}
/** Animates the dot to the given scale. */
private void animateDotScale(float toScale) {
boolean isDotScaleChanging = Float.compare(mDotScale, toScale) != 0;
// Don't restart the animation if we're already animating to the given value or if the dot
// scale is not changing
if ((mDotIsAnimating && mAnimatingToDotScale == toScale) || !isDotScaleChanging) {
return;
}
/**
* Animates the dot to the given scale.
*/
private void animateDotScale() {
float toScale = shouldDrawDot() ? 1f : 0f;
mDotIsAnimating = true;
// Don't restart the animation if we're already animating to the given value.
if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
mDotIsAnimating = false;
return;
}
mAnimatingToDotScale = toScale;
final boolean showDot = toScale > 0f;
// Do NOT wait until after animation ends to setShowDot
// to avoid overriding more recent showDot states.
clearAnimation();
animate()
.setDuration(SCALE_ANIMATION_DURATION_MS)
.setDuration(200)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.setUpdateListener((valueAnimator) -> {
float fraction = valueAnimator.getAnimatedFraction();
@@ -472,42 +333,9 @@ public class BubbleView extends ConstraintLayout {
}).start();
}
/**
* Returns the distance from the top left corner of this bubble view to the center of its dot.
*/
public PointF getDotCenter() {
float[] dotPosition =
mOnLeft ? mDotRenderer.getLeftDotPosition() : mDotRenderer.getRightDotPosition();
getDrawingRect(mTempBounds);
float dotCenterX = mTempBounds.width() * dotPosition[0];
float dotCenterY = mTempBounds.height() * dotPosition[1];
return new PointF(dotCenterX, dotCenterY);
}
/** Returns the dot color. */
public int getDotColor() {
return mDotColor;
}
@Override
public String toString() {
String toString = mBubble != null ? mBubble.getKey() : "null";
return "BubbleView{" + toString + "}";
}
/** Interface for BubbleView to communicate with its controller */
public interface Controller {
/** Get current bubble bar {@link BubbleBarLocation} */
BubbleBarLocation getBubbleBarLocation();
/** This bubble should be dismissed */
void dismiss(BubbleView bubble);
/** Collapse the bubble bar */
void collapse();
/** Request bubble bar location to be updated to the given location */
void updateBubbleBarLocation(BubbleBarLocation location,
@BubbleBarLocation.UpdateSource int source);
}
}
@@ -16,21 +16,14 @@
package com.android.launcher3.taskbar.bubbles.animation
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.view.View
import android.view.View.VISIBLE
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.launcher3.R
import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
import com.android.launcher3.taskbar.bubbles.BubbleBarParentViewHeightUpdateNotifier
import com.android.launcher3.taskbar.bubbles.BubbleBarView
import com.android.launcher3.taskbar.bubbles.BubbleStashController
import com.android.launcher3.taskbar.bubbles.BubbleView
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController
import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutMessage
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController
import com.android.wm.shell.shared.animation.PhysicsAnimator
/** Handles animations for bubble bar bubbles. */
@@ -39,69 +32,26 @@ class BubbleBarViewAnimator
constructor(
private val bubbleBarView: BubbleBarView,
private val bubbleStashController: BubbleStashController,
private val bubbleBarFlyoutController: BubbleBarFlyoutController,
private val bubbleBarParentViewHeightUpdateNotifier: BubbleBarParentViewHeightUpdateNotifier,
private val onExpanded: Runnable,
private val onBubbleBarVisible: Runnable,
private val scheduler: Scheduler = HandlerScheduler(bubbleBarView),
private val scheduler: Scheduler = HandlerScheduler(bubbleBarView)
) {
private var animatingBubble: AnimatingBubble? = null
private val bubbleBarBounceDistanceInPx =
bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
fun hasAnimation() = animatingBubble != null
val isAnimating: Boolean
get() {
val animatingBubble = animatingBubble ?: return false
return animatingBubble.state != AnimatingBubble.State.CREATED
}
private var interceptedHandleAnimator = false
private companion object {
/** The time to show the flyout. */
const val FLYOUT_DELAY_MS: Long = 3000
const val FLYOUT_DELAY_MS: Long = 2500
/** The initial scale Y value that the new bubble is set to before the animation starts. */
const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f
/** The minimum alpha value to make the bubble bar touchable. */
const val MIN_ALPHA_FOR_TOUCHABLE = 0.5f
/** The duration of the bounce animation. */
const val BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS = 250L
}
/** Wrapper around the animating bubble with its show and hide animations. */
private data class AnimatingBubble(
val bubbleView: BubbleView,
val showAnimation: Runnable,
val hideAnimation: Runnable,
val expand: Boolean,
val state: State = State.CREATED,
) {
/**
* The state of the animation.
*
* The animation is initially created but will be scheduled later using the [Scheduler].
*
* The normal uninterrupted cycle is for the bubble notification to animate in, then be in a
* transient state and eventually to animate out.
*
* However different events, such as touch and external signals, may cause the animation to
* end earlier.
*/
enum class State {
/** The animation is created but not started yet. */
CREATED,
/** The bubble notification is animating in. */
ANIMATING_IN,
/** The bubble notification is now fully showing and waiting to be hidden. */
IN,
/** The bubble notification is animating out. */
ANIMATING_OUT,
}
}
val hideAnimation: Runnable
)
/** An interface for scheduling jobs. */
interface Scheduler {
@@ -135,34 +85,19 @@ constructor(
private val springConfig =
PhysicsAnimator.SpringConfig(
stiffness = SpringForce.STIFFNESS_LOW,
dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
)
private fun cancelAnimationIfPending() {
val animatingBubble = animatingBubble ?: return
if (animatingBubble.state != AnimatingBubble.State.CREATED) return
scheduler.cancel(animatingBubble.showAnimation)
scheduler.cancel(animatingBubble.hideAnimation)
}
/** Animates a bubble for the state where the bubble bar is stashed. */
fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) {
if (isAnimating) {
interruptAndUpdateAnimatingBubble(b.view, isExpanding)
return
}
cancelAnimationIfPending()
fun animateBubbleInForStashed(b: BubbleBarBubble) {
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
// the animation of a new bubble is divided into 2 parts. The first part transforms the
// handle to the bubble bar and then shows the flyout. The second part hides the flyout and
// transforms the bubble bar back to the handle.
// the animation of a new bubble is divided into 2 parts. The first part shows the bubble
// and the second part hides it after a delay.
val showAnimation = buildHandleToBubbleBarAnimation()
val hideAnimation = if (isExpanding) Runnable {} else buildBubbleBarToHandleAnimation()
animatingBubble =
AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
val hideAnimation = buildBubbleBarToHandleAnimation()
animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
scheduler.post(showAnimation)
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
@@ -181,48 +116,40 @@ constructor(
* 3. The third part is the overshoot of the spring animation, where we make the bubble fully
* visible which helps avoiding further updates when we re-enter the second part.
*/
private fun buildHandleToBubbleBarAnimation(initialVelocity: Float? = null) = Runnable {
moveToState(AnimatingBubble.State.ANIMATING_IN)
// prepare the bubble bar for the animation if we're starting fresh
if (initialVelocity == null) {
bubbleBarView.visibility = VISIBLE
bubbleBarView.alpha = 0f
bubbleBarView.translationY = 0f
bubbleBarView.scaleX = 1f
bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
bubbleBarView.setBackgroundScaleX(1f)
bubbleBarView.setBackgroundScaleY(1f)
bubbleBarView.relativePivotY = 0.5f
}
private fun buildHandleToBubbleBarAnimation() = Runnable {
// prepare the bubble bar for the animation
bubbleBarView.onAnimatingBubbleStarted()
bubbleBarView.visibility = VISIBLE
bubbleBarView.alpha = 0f
bubbleBarView.translationY = 0f
bubbleBarView.scaleX = 1f
bubbleBarView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y
bubbleBarView.relativePivotY = 0.5f
// this is the offset between the center of the bubble bar and the center of the stash
// handle. when the handle becomes invisible and we start animating in the bubble bar,
// the translation y is offset by this value to make the transition from the handle to the
// bar smooth.
val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
val stashedHandleTranslationYForAnimation =
bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
val offset = bubbleStashController.diffBetweenHandleAndBarCenters
val stashedHandleTranslationY =
bubbleStashController.getHandleTranslationY() ?: return@Runnable
val translationTracker = TranslationTracker(stashedHandleTranslationY)
bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
// this is the total distance that both the stashed handle and the bubble will be traveling
// at the end of the animation the bubble bar will be positioned in the same place when it
// shows while we're in an app.
val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable
val animator = bubbleStashController.stashedHandlePhysicsAnimator
animator.setDefaultSpringConfig(springConfig)
animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY, initialVelocity ?: 0f)
animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY)
animator.addUpdateListener { handle, values ->
val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
if (animatingBubble == null) return@addUpdateListener
when {
ty >= stashedHandleTranslationYForAnimation -> {
ty >= stashedHandleTranslationY -> {
// we're in the first leg of the animation. only animate the handle. the bubble
// bar remains hidden during this part of the animation
// map the path [0, stashedHandleTranslationY] to [0,1]
val fraction = ty / stashedHandleTranslationYForAnimation
val fraction = ty / stashedHandleTranslationY
handle.alpha = 1 - fraction
}
ty >= totalTranslationY -> {
@@ -236,8 +163,8 @@ constructor(
if (bubbleBarView.alpha != 1f) {
// map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1]
val fraction =
(ty - stashedHandleTranslationYForAnimation) /
(totalTranslationY - stashedHandleTranslationYForAnimation)
(ty - stashedHandleTranslationY) /
(totalTranslationY - stashedHandleTranslationY)
bubbleBarView.alpha = fraction
bubbleBarView.scaleY =
BUBBLE_ANIMATION_INITIAL_SCALE_Y +
@@ -257,17 +184,18 @@ constructor(
bubbleStashController.updateTaskbarTouchRegion()
}
}
translationTracker.updateTyAndExpandIfNeeded(ty)
}
animator.addEndListener { _, _, _, canceled, _, _, _ ->
// if the show animation was canceled, also cancel the hide animation. this is typically
// canceled in this class, but could potentially be canceled elsewhere.
if (canceled || animatingBubble?.expand == true) {
cancelHideAnimation()
if (canceled) {
val hideAnimation = animatingBubble?.hideAnimation ?: return@addEndListener
scheduler.cancel(hideAnimation)
animatingBubble = null
bubbleBarView.onAnimatingBubbleCompleted()
bubbleBarView.relativePivotY = 1f
return@addEndListener
}
setupAndShowFlyout()
// the bubble bar is now fully settled in. update taskbar touch region so it's touchable
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -290,14 +218,13 @@ constructor(
*/
private fun buildBubbleBarToHandleAnimation() = Runnable {
if (animatingBubble == null) return@Runnable
moveToState(AnimatingBubble.State.ANIMATING_OUT)
val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
val offset = bubbleStashController.diffBetweenHandleAndBarCenters
val stashedHandleTranslationY =
bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
bubbleStashController.stashedHandleTranslationForNewBubbleAnimation
// this is the total distance that both the stashed handle and the bar will be traveling
val totalTranslationY = bubbleStashController.bubbleBarTranslationYForTaskbar + offset
bubbleStashController.setHandleTranslationY(totalTranslationY)
val animator = bubbleStashController.getStashedHandlePhysicsAnimator() ?: return@Runnable
val animator = bubbleStashController.stashedHandlePhysicsAnimator
animator.setDefaultSpringConfig(springConfig)
animator.spring(DynamicAnimation.TRANSLATION_Y, 0f)
animator.addUpdateListener { handle, values ->
@@ -333,430 +260,88 @@ constructor(
}
}
}
animator.addEndListener { _, _, _, canceled, _, finalVelocity, _ ->
// PhysicsAnimator calls the end listeners when the animation is replaced with a new one
// if we're not in ANIMATING_OUT state, then this animation never started and we should
// return
if (animatingBubble?.state != AnimatingBubble.State.ANIMATING_OUT) return@addEndListener
if (interceptedHandleAnimator) {
interceptedHandleAnimator = false
// post this to give a PhysicsAnimator a chance to clean up its internal listeners.
// otherwise this end listener will be called as soon as we create a new spring
// animation
scheduler.post(buildHandleToBubbleBarAnimation(initialVelocity = finalVelocity))
return@addEndListener
}
clearAnimatingBubble()
animator.addEndListener { _, _, _, canceled, _, _, _ ->
animatingBubble = null
if (!canceled) bubbleStashController.stashBubbleBarImmediate()
bubbleBarView.onAnimatingBubbleCompleted()
bubbleBarView.relativePivotY = 1f
bubbleBarView.scaleY = 1f
bubbleStashController.updateTaskbarTouchRegion()
}
val bubble = animatingBubble?.bubbleView?.bubble as? BubbleBarBubble
val flyout = bubble?.flyoutMessage
if (flyout != null) {
bubbleBarFlyoutController.collapseFlyout {
onFlyoutRemoved()
animator.start()
}
} else {
animator.start()
}
animator.start()
}
/** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
fun animateToInitialState(
b: BubbleBarBubble,
isInApp: Boolean,
isExpanding: Boolean,
isDragging: Boolean = false,
) {
fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
// the animation of a new bubble is divided into 2 parts. The first part slides in the
// bubble bar and shows the flyout. The second part hides the flyout and transforms the
// bubble bar to the handle if we're in an app.
val showAnimation = buildBubbleBarSpringInAnimation()
// the animation of a new bubble is divided into 2 parts. The first part shows the bubble
// and the second part hides it after a delay if we are in an app.
val showAnimation = buildBubbleBarBounceAnimation()
val hideAnimation =
if (isInApp && !isExpanding && !isDragging) {
if (isInApp && !isExpanding) {
buildBubbleBarToHandleAnimation()
} else {
// in this case the bubble bar remains visible so not much to do. once we implement
// the flyout we'll update this runnable to hide it.
Runnable {
collapseFlyoutAndUpdateState()
if (isDragging) return@Runnable
animatingBubble = null
bubbleStashController.showBubbleBarImmediate()
bubbleBarView.onAnimatingBubbleCompleted()
bubbleStashController.updateTaskbarTouchRegion()
}
}
animatingBubble =
AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
scheduler.post(showAnimation)
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
private fun buildBubbleBarSpringInAnimation() = Runnable {
moveToState(AnimatingBubble.State.ANIMATING_IN)
private fun buildBubbleBarBounceAnimation() = Runnable {
// prepare the bubble bar for the animation
bubbleBarView.onAnimatingBubbleStarted()
bubbleBarView.translationY = bubbleBarView.height.toFloat()
bubbleBarView.visibility = VISIBLE
onBubbleBarVisible.run()
bubbleBarView.alpha = 1f
bubbleBarView.scaleX = 1f
bubbleBarView.scaleY = 1f
bubbleBarView.setBackgroundScaleX(1f)
bubbleBarView.setBackgroundScaleY(1f)
val translationTracker = TranslationTracker(bubbleBarView.translationY)
val animator = PhysicsAnimator.getInstance(bubbleBarView)
animator.setDefaultSpringConfig(springConfig)
animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY)
animator.addUpdateListener { _, values ->
val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
translationTracker.updateTyAndExpandIfNeeded(ty)
bubbleStashController.updateTaskbarTouchRegion()
}
animator.addUpdateListener { _, _ -> bubbleStashController.updateTaskbarTouchRegion() }
animator.addEndListener { _, _, _, _, _, _, _ ->
if (animatingBubble?.expand == true) {
cancelHideAnimation()
} else {
setupAndShowFlyout()
}
// the bubble bar is now fully settled in. update taskbar touch region so it's touchable
bubbleStashController.updateTaskbarTouchRegion()
}
animator.start()
}
fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) {
if (isAnimating) {
interruptAndUpdateAnimatingBubble(b.view, isExpanding)
return
}
cancelAnimationIfPending()
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
// first bounce the bubble bar and show the flyout. Then hide the flyout.
val showAnimation = buildBubbleBarBounceAnimation()
val hideAnimation = Runnable {
collapseFlyoutAndUpdateState()
bubbleStashController.showBubbleBarImmediate()
bubbleStashController.updateTaskbarTouchRegion()
}
animatingBubble =
AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
scheduler.post(showAnimation)
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
private fun collapseFlyoutAndUpdateState() {
moveToState(AnimatingBubble.State.ANIMATING_OUT)
bubbleBarFlyoutController.collapseFlyout {
onFlyoutRemoved()
clearAnimatingBubble()
}
}
/**
* The bubble bar animation when it is collapsed is divided into 2 chained animations. The first
* animation is a regular accelerate animation that moves the bubble bar upwards. When it ends
* the bubble bar moves back to its initial position with a spring animation.
*/
private fun buildBubbleBarBounceAnimation() = Runnable {
moveToState(AnimatingBubble.State.ANIMATING_IN)
val ty = bubbleStashController.bubbleBarTranslationY
val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView)
springBackAnimation.setDefaultSpringConfig(springConfig)
springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty)
springBackAnimation.addEndListener { _, _, _, _, _, _, _ ->
if (animatingBubble?.expand == true) {
expandBubbleBar()
cancelHideAnimation()
} else {
setupAndShowFlyout()
}
}
// animate the bubble bar up and start the spring back down animation when it ends.
ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx)
.withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS)
.withEndAction {
springBackAnimation.start()
if (animatingBubble?.expand == true) expandBubbleBar()
}
.start()
}
private fun setupAndShowFlyout() {
val bubbleView = animatingBubble?.bubbleView
val bubble = bubbleView?.bubble as? BubbleBarBubble
val flyout = bubble?.flyoutMessage
if (flyout != null) {
bubbleBarFlyoutController.setUpAndShowFlyout(
BubbleBarFlyoutMessage(flyout.icon, flyout.title, flyout.message),
onInit = { bubbleView.suppressDotForBubbleUpdate() },
onEnd = {
moveToState(AnimatingBubble.State.IN)
bubbleStashController.updateTaskbarTouchRegion()
},
)
} else {
moveToState(AnimatingBubble.State.IN)
}
}
private fun cancelFlyout() {
animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ true)
bubbleBarFlyoutController.cancelFlyout { bubbleStashController.updateTaskbarTouchRegion() }
}
private fun onFlyoutRemoved() {
animatingBubble?.bubbleView?.unsuppressDotForBubbleUpdate(/* animate= */ false)
bubbleStashController.updateTaskbarTouchRegion()
}
/** Interrupts the animation due to touching the bubble bar or flyout. */
fun interruptForTouch() {
animatingBubble?.hideAnimation?.let { scheduler.cancel(it) }
/** Handles touching the animating bubble bar. */
fun onBubbleBarTouchedWhileAnimating() {
PhysicsAnimator.getInstance(bubbleBarView).cancelIfRunning()
bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
cancelFlyout()
resetBubbleBarPropertiesOnInterrupt()
clearAnimatingBubble()
bubbleStashController.stashedHandlePhysicsAnimator.cancelIfRunning()
val hideAnimation = animatingBubble?.hideAnimation ?: return
scheduler.cancel(hideAnimation)
bubbleBarView.onAnimatingBubbleCompleted()
bubbleBarView.relativePivotY = 1f
animatingBubble = null
}
/** Notifies the animator that the taskbar area was touched during an animation. */
fun onStashStateChangingWhileAnimating() {
animatingBubble?.hideAnimation?.let { scheduler.cancel(it) }
cancelFlyout()
clearAnimatingBubble()
bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
resetBubbleBarPropertiesOnInterrupt()
bubbleStashController.onNewBubbleAnimationInterrupted(
/* isStashed= */ bubbleStashController.isStashed,
bubbleBarView.translationY,
)
}
/** Interrupts the animation due to the IME becoming visible. */
fun interruptForIme() {
cancelFlyout()
val hideAnimation = animatingBubble?.hideAnimation ?: return
scheduler.cancel(hideAnimation)
animatingBubble = null
bubbleStashController.getStashedHandlePhysicsAnimator().cancelIfRunning()
resetBubbleBarPropertiesOnInterrupt()
// stash the bubble bar since the IME is now visible
bubbleStashController.stashedHandlePhysicsAnimator.cancel()
bubbleBarView.onAnimatingBubbleCompleted()
bubbleBarView.relativePivotY = 1f
bubbleStashController.onNewBubbleAnimationInterrupted(
/* isStashed= */ true,
bubbleBarView.translationY,
/* isStashed= */ bubbleBarView.alpha == 0f,
bubbleBarView.translationY
)
}
fun expandedWhileAnimating() {
val animatingBubble = animatingBubble ?: return
this.animatingBubble = animatingBubble.copy(expand = true)
// if we're fully in and waiting to hide, cancel the hide animation and clean up
if (animatingBubble.state == AnimatingBubble.State.IN) {
cancelFlyout()
expandBubbleBar()
cancelHideAnimation()
}
}
private fun interruptAndUpdateAnimatingBubble(bubbleView: BubbleView, isExpanding: Boolean) {
val animatingBubble = animatingBubble ?: return
when (animatingBubble.state) {
AnimatingBubble.State.CREATED -> {} // nothing to do since the animation hasn't started
AnimatingBubble.State.ANIMATING_IN ->
updateAnimationWhileAnimatingIn(animatingBubble, bubbleView, isExpanding)
AnimatingBubble.State.IN ->
updateAnimationWhileIn(animatingBubble, bubbleView, isExpanding)
AnimatingBubble.State.ANIMATING_OUT ->
updateAnimationWhileAnimatingOut(animatingBubble, bubbleView, isExpanding)
}
}
private fun updateAnimationWhileAnimatingIn(
animatingBubble: AnimatingBubble,
bubbleView: BubbleView,
isExpanding: Boolean,
) {
this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
if (!bubbleBarFlyoutController.hasFlyout()) {
// if the flyout does not yet exist, then we're only animating the bubble bar.
// the animating bubble has been updated, so the when the flyout expands it will
// show the right message. we only need to update the dot visibility.
bubbleView.updateDotVisibility(/* animate= */ !bubbleStashController.isStashed)
return
}
val bubble = bubbleView.bubble as? BubbleBarBubble
val flyout = bubble?.flyoutMessage
if (flyout != null) {
// the flyout is currently expanding and we need to update it with new data
bubbleView.suppressDotForBubbleUpdate()
bubbleBarFlyoutController.updateFlyoutWhileExpanding(flyout)
} else {
// the flyout is expanding but we don't have new flyout data to update it with,
// so cancel the expanding flyout.
cancelFlyout()
}
}
private fun updateAnimationWhileIn(
animatingBubble: AnimatingBubble,
bubbleView: BubbleView,
isExpanding: Boolean,
) {
// unsuppress the current bubble because we are about to hide its flyout
animatingBubble.bubbleView.unsuppressDotForBubbleUpdate(/* animate= */ false)
this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
// we're currently idle, waiting for the hide animation to start. update the flyout
// data and reschedule the hide animation to run later to give the user a chance to
// see the new flyout.
val hideAnimation = animatingBubble.hideAnimation
scheduler.cancel(hideAnimation)
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
val bubble = bubbleView.bubble as? BubbleBarBubble
val flyout = bubble?.flyoutMessage
if (flyout != null) {
bubbleView.suppressDotForBubbleUpdate()
bubbleBarFlyoutController.updateFlyoutFullyExpanded(flyout) {
bubbleStashController.updateTaskbarTouchRegion()
}
} else {
cancelFlyout()
}
}
private fun updateAnimationWhileAnimatingOut(
animatingBubble: AnimatingBubble,
bubbleView: BubbleView,
isExpanding: Boolean,
) {
// unsuppress the current bubble because we are about to hide its flyout
animatingBubble.bubbleView.unsuppressDotForBubbleUpdate(/* animate= */ false)
this.animatingBubble = animatingBubble.copy(bubbleView = bubbleView, expand = isExpanding)
// the hide animation already started so it can't be canceled, just post it again
val hideAnimation = animatingBubble.hideAnimation
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
val bubble = bubbleView.bubble as? BubbleBarBubble
val flyout = bubble?.flyoutMessage
if (bubbleBarFlyoutController.hasFlyout()) {
// the flyout is collapsing. update it with the new flyout
if (flyout != null) {
moveToState(AnimatingBubble.State.ANIMATING_IN)
bubbleView.suppressDotForBubbleUpdate()
bubbleBarFlyoutController.updateFlyoutWhileCollapsing(flyout) {
moveToState(AnimatingBubble.State.IN)
bubbleStashController.updateTaskbarTouchRegion()
}
} else {
cancelFlyout()
moveToState(AnimatingBubble.State.IN)
}
} else {
// the flyout is already gone. if we're animating the handle cancel it. the
// animation itself can handle morphing back into the bubble bar and restarting
// and show the flyout.
val handleAnimator = bubbleStashController.getStashedHandlePhysicsAnimator()
if (handleAnimator != null && handleAnimator.isRunning()) {
interceptedHandleAnimator = true
handleAnimator.cancel()
}
// if we're not animating the handle, then the hide animation simply hides the
// flyout, but if the flyout is gone then the animation has ended.
}
}
private fun cancelHideAnimation() {
val hideAnimation = animatingBubble?.hideAnimation ?: return
scheduler.cancel(hideAnimation)
clearAnimatingBubble()
bubbleBarView.relativePivotY = 1f
bubbleStashController.showBubbleBarImmediate()
}
private fun resetBubbleBarPropertiesOnInterrupt() {
bubbleBarView.relativePivotY = 1f
bubbleBarView.scaleX = 1f
bubbleBarView.scaleY = 1f
}
private fun <T> PhysicsAnimator<T>?.cancelIfRunning() {
if (this?.isRunning() == true) cancel()
}
private fun ObjectAnimator.withDuration(duration: Long): ObjectAnimator {
setDuration(duration)
return this
}
private fun ObjectAnimator.withEndAction(endAction: () -> Unit): ObjectAnimator {
addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
endAction()
}
}
)
return this
}
private fun moveToState(state: AnimatingBubble.State) {
val animatingBubble = this.animatingBubble ?: return
this.animatingBubble = animatingBubble.copy(state = state)
if (state == AnimatingBubble.State.ANIMATING_IN) {
bubbleBarParentViewHeightUpdateNotifier.updateTopBoundary()
}
}
private fun clearAnimatingBubble() {
animatingBubble = null
bubbleBarParentViewHeightUpdateNotifier.updateTopBoundary()
}
private fun expandBubbleBar() {
bubbleBarView.isExpanded = true
onExpanded.run()
}
/**
* Tracks the translation Y of the bubble bar during the animation. When the bubble bar expands
* as part of the animation, the expansion should start after the bubble bar reaches the peak
* position.
*/
private inner class TranslationTracker(initialTy: Float) {
private var previousTy = initialTy
private var startedExpanding = false
private var reachedPeak = false
fun updateTyAndExpandIfNeeded(ty: Float) {
if (!reachedPeak) {
// the bubble bar is positioned at the bottom of the screen and moves up using
// negative ty values. the peak is reached the first time we see a value that is
// greater than the previous.
if (ty > previousTy) {
reachedPeak = true
}
}
val expand = animatingBubble?.expand ?: false
if (reachedPeak && expand && !startedExpanding) {
expandBubbleBar()
startedExpanding = true
}
previousTy = ty
}
private fun <T> PhysicsAnimator<T>.cancelIfRunning() {
if (isRunning()) cancel()
}
}
@@ -13,13 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.taskbar.customization
import androidx.annotation.Dimension
/**
* Interface to be implemented by all taskbar container to expose [spaceNeeded] for each container.
*/
interface TaskbarContainer {
@get:Dimension(unit = Dimension.DP) val spaceNeeded: Int
/** Enums for all feature container that taskbar supports. */
enum class TaskbarContainer {
ALL_APPS,
DIVIDER,
APP_ICONS,
RECENTS,
NAV_BUTTONS,
BUBBLES,
}
@@ -16,48 +16,29 @@
package com.android.launcher3.taskbar.customization
import com.android.launcher3.Flags.enableRecentsInTaskbar
import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.launcher3.taskbar.TaskbarControllers
import com.android.launcher3.taskbar.TaskbarRecentAppsController
import com.android.launcher3.util.DisplayController
/** Evaluates all the features taskbar can have. */
class TaskbarFeatureEvaluator
private constructor(private val taskbarActivityContext: TaskbarActivityContext) {
class TaskbarFeatureEvaluator(
private val taskbarActivityContext: TaskbarActivityContext,
private val taskbarControllers: TaskbarControllers,
) {
val hasAllApps = true
val hasAppIcons = true
val hasBubbles = false
val hasNavButtons = taskbarActivityContext.isThreeButtonNav
val isRecentsEnabled: Boolean
get() = enableRecentsInTaskbar()
val hasRecents: Boolean
get() = taskbarControllers.taskbarRecentAppsController.isEnabled
val hasDivider: Boolean
get() = enableTaskbarPinning() || isRecentsEnabled
get() = enableTaskbarPinning() || hasRecents
val isTransient: Boolean
get() = taskbarActivityContext.isTransientTaskbar
val isLandscape: Boolean
get() = taskbarActivityContext.deviceProfile.isLandscape
val supportsPinningPopup: Boolean
get() = !hasNavButtons
fun onDestroy() {
taskbarFeatureEvaluator = null
}
companion object {
@Volatile private var taskbarFeatureEvaluator: TaskbarFeatureEvaluator? = null
@JvmStatic
fun getInstance(taskbarActivityContext: TaskbarActivityContext): TaskbarFeatureEvaluator {
synchronized(this) {
if (taskbarFeatureEvaluator == null) {
taskbarFeatureEvaluator = TaskbarFeatureEvaluator(taskbarActivityContext)
}
return taskbarFeatureEvaluator!!
}
}
}
get() = DisplayController.isTransientTaskbar(taskbarActivityContext)
}
@@ -19,35 +19,23 @@ package com.android.launcher3.taskbar.customization
/** Taskbar Icon Specs */
object TaskbarIconSpecs {
// Mapping of visual icon size to icon specs value http://b/235886078
val iconSize40dp = TaskbarIconSize(44)
val iconSize44dp = TaskbarIconSize(48)
val iconSize48dp = TaskbarIconSize(52)
val iconSize52dp = TaskbarIconSize(57)
val iconSize40dp = TaskbarIconSize(40)
val iconSize44dp = TaskbarIconSize(44)
val iconSize48dp = TaskbarIconSize(48)
val iconSize52dp = TaskbarIconSize(52)
val transientTaskbarIconSizes = arrayOf(iconSize44dp, iconSize48dp, iconSize52dp)
val defaultPersistentIconSize = iconSize40dp
val defaultTransientIconSize = iconSize44dp
val minimumIconSize = iconSize40dp
val defaultPersistentIconMargin = TaskbarIconMarginSize(6)
val defaultTransientIconMargin = TaskbarIconMarginSize(12)
val minimumTaskbarIconTouchSize = TaskbarIconSize(48)
val transientOrPinnedTaskbarIconPaddingSize = iconSize52dp
// defined as row, columns
val transientTaskbarIconSizeByGridSize =
mapOf(
TransientTaskbarIconSizeKey(6, 5, false) to iconSize52dp,
TransientTaskbarIconSizeKey(6, 5, true) to iconSize52dp,
TransientTaskbarIconSizeKey(4, 4, false) to iconSize48dp,
TransientTaskbarIconSizeKey(4, 4, true) to iconSize52dp,
TransientTaskbarIconSizeKey(4, 5, false) to iconSize48dp,
TransientTaskbarIconSizeKey(4, 5, true) to iconSize48dp,
TransientTaskbarIconSizeKey(5, 5, false) to iconSize44dp,
TransientTaskbarIconSizeKey(5, 5, true) to iconSize44dp,
Pair(6, 5) to iconSize52dp,
Pair(4, 5) to iconSize48dp,
Pair(5, 4) to iconSize48dp,
Pair(4, 4) to iconSize48dp,
Pair(5, 6) to iconSize44dp,
)
}
@@ -16,43 +16,13 @@
package com.android.launcher3.taskbar.customization
import com.android.launcher3.taskbar.TaskbarActivityContext
/** Evaluates the taskbar specs based on the taskbar grid size and the taskbar icon size. */
class TaskbarSpecsEvaluator(
private val taskbarActivityContext: TaskbarActivityContext,
private val taskbarFeatureEvaluator: TaskbarFeatureEvaluator,
numRows: Int = taskbarActivityContext.deviceProfile.inv.numRows,
numColumns: Int = taskbarActivityContext.deviceProfile.inv.numColumns,
) {
var taskbarIconSize: TaskbarIconSize = getIconSizeByGrid(numColumns, numRows)
val numShownHotseatIcons
get() = taskbarActivityContext.deviceProfile.numShownHotseatIcons
class TaskbarSpecsEvaluator(private val taskbarFeatureEvaluator: TaskbarFeatureEvaluator) {
// TODO(b/341146605) : initialize it to taskbar container in later cl.
private var taskbarContainer: List<TaskbarContainer> = emptyList()
val taskbarIconPadding: Int =
if (
TaskbarIconSpecs.transientOrPinnedTaskbarIconPaddingSize.size > taskbarIconSize.size &&
!taskbarFeatureEvaluator.hasNavButtons
) {
(TaskbarIconSpecs.iconSize52dp.size - taskbarIconSize.size) / 2
} else {
0
}
val taskbarIconMargin: TaskbarIconMarginSize =
if (taskbarFeatureEvaluator.isTransient) {
TaskbarIconSpecs.defaultTransientIconMargin
} else {
TaskbarIconSpecs.defaultPersistentIconMargin
}
fun getIconSizeByGrid(columns: Int, rows: Int): TaskbarIconSize {
fun getIconSizeByGrid(row: Int, column: Int): TaskbarIconSize {
return if (taskbarFeatureEvaluator.isTransient) {
TaskbarIconSpecs.transientTaskbarIconSizeByGridSize.getOrDefault(
TransientTaskbarIconSizeKey(columns, rows, taskbarFeatureEvaluator.isLandscape),
Pair(row, column),
TaskbarIconSpecs.defaultTransientIconSize,
)
} else {
@@ -66,11 +36,8 @@ class TaskbarSpecsEvaluator(
val currentIconSizeIndex = TaskbarIconSpecs.transientTaskbarIconSizes.indexOf(iconSize)
// return the current icon size if supplied icon size is unknown or we have reached the
// min icon size.
return if (currentIconSizeIndex == -1 || currentIconSizeIndex == 0) {
iconSize
} else {
TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex - 1]
}
return if (currentIconSizeIndex == -1 || currentIconSizeIndex == 0) iconSize
else TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex - 1]
}
fun getIconSizeStepUp(iconSize: TaskbarIconSize): TaskbarIconSize {
@@ -85,28 +52,9 @@ class TaskbarSpecsEvaluator(
) {
iconSize
} else {
TaskbarIconSpecs.transientTaskbarIconSizes[currentIconSizeIndex + 1]
TaskbarIconSpecs.transientTaskbarIconSizes.get(currentIconSizeIndex + 1)
}
}
// TODO(jagrutdesai) : Call this in init once the containers are ready.
private fun calculateTaskbarIconSize() {
while (
taskbarIconSize != TaskbarIconSpecs.minimumIconSize &&
taskbarActivityContext.transientTaskbarBounds.width() <
calculateSpaceNeeded(taskbarContainer)
) {
taskbarIconSize = getIconSizeStepDown(taskbarIconSize)
}
}
private fun calculateSpaceNeeded(containers: List<TaskbarContainer>): Int {
return containers.sumOf { it.spaceNeeded }
}
}
data class TaskbarIconSize(val size: Int)
data class TransientTaskbarIconSizeKey(val columns: Int, val rows: Int, val isLandscape: Boolean)
data class TaskbarIconMarginSize(val size: Int)
@@ -27,6 +27,7 @@ import android.widget.Space
import com.android.launcher3.DeviceProfile
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter
/**
@@ -47,7 +48,7 @@ abstract class AbstractNavButtonLayoutter(
protected val startContextualContainer: ViewGroup,
protected val imeSwitcher: ImageView?,
protected val a11yButton: ImageView?,
protected val space: Space?,
protected val space: Space?
) : NavButtonLayoutter {
protected val homeButton: ImageView? = navButtonContainer.findViewById(R.id.home)
protected val recentsButton: ImageView? = navButtonContainer.findViewById(R.id.recent_apps)
@@ -68,34 +69,26 @@ abstract class AbstractNavButtonLayoutter(
val params =
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
params.gravity = Gravity.CENTER
return params
}
/**
* Adjusts the layout parameters of the nav bar container for setup in phone mode.
*
* @param nearestTouchFrameLayoutParams The layout parameters of the navButtonsView, which is
* the ViewGroup that contains start, end, nav button ViewGroups
* @param deviceProfile The device profile containing information about the device's
* configuration.
*/
fun adjustForSetupInPhoneMode(
nearestTouchFrameLayoutParams: FrameLayout.LayoutParams,
deviceProfile: DeviceProfile,
navButtonsLayoutParams: FrameLayout.LayoutParams,
navButtonsViewLayoutParams: FrameLayout.LayoutParams,
deviceProfile: DeviceProfile
) {
val phoneOrPortraitSetupMargin =
resources.getDimensionPixelSize(R.dimen.taskbar_contextual_button_suw_margin)
nearestTouchFrameLayoutParams.marginStart = phoneOrPortraitSetupMargin
nearestTouchFrameLayoutParams.bottomMargin =
navButtonsLayoutParams.marginStart = phoneOrPortraitSetupMargin
navButtonsLayoutParams.bottomMargin =
if (!deviceProfile.isLandscape) 0
else
phoneOrPortraitSetupMargin -
resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size) / 2
nearestTouchFrameLayoutParams.height =
resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size) / 2
navButtonsViewLayoutParams.height =
resources.getDimensionPixelSize(R.dimen.taskbar_contextual_button_suw_height)
}
@@ -104,7 +97,7 @@ abstract class AbstractNavButtonLayoutter(
buttonSize: Int,
barAxisMarginStart: Int,
barAxisMarginEnd: Int,
gravity: Int,
gravity: Int
) {
val contextualContainerParams =
FrameLayout.LayoutParams(buttonSize, ViewGroup.LayoutParams.MATCH_PARENT)
@@ -66,7 +66,7 @@ class NavButtonLayoutFactory {
isInSetup: Boolean,
isThreeButtonNav: Boolean,
phoneMode: Boolean,
@Rotation surfaceRotation: Int,
@Rotation surfaceRotation: Int
): NavButtonLayoutter {
val navButtonContainer =
navButtonsView.requireViewById<LinearLayout>(ID_END_NAV_BUTTONS)
@@ -77,18 +77,6 @@ class NavButtonLayoutFactory {
val isPhoneNavMode = phoneMode && isThreeButtonNav
val isPhoneGestureMode = phoneMode && !isThreeButtonNav
return when {
isInSetup -> {
SetupNavLayoutter(
resources,
navButtonsView,
navButtonContainer,
endContextualContainer,
startContextualContainer,
imeSwitcher,
a11yButton,
space,
)
}
isPhoneNavMode -> {
if (!deviceProfile.isLandscape) {
navButtonsView.setIsVertical(false)
@@ -99,7 +87,7 @@ class NavButtonLayoutFactory {
startContextualContainer,
imeSwitcher,
a11yButton,
space,
space
)
} else if (surfaceRotation == ROTATION_90) {
navButtonsView.setIsVertical(true)
@@ -110,7 +98,7 @@ class NavButtonLayoutFactory {
startContextualContainer,
imeSwitcher,
a11yButton,
space,
space
)
} else {
navButtonsView.setIsVertical(true)
@@ -121,23 +109,36 @@ class NavButtonLayoutFactory {
startContextualContainer,
imeSwitcher,
a11yButton,
space,
space
)
}
}
isPhoneGestureMode -> {
PhoneGestureLayoutter(
resources,
navButtonsView,
navButtonContainer,
endContextualContainer,
startContextualContainer,
imeSwitcher,
a11yButton,
space,
space
)
}
deviceProfile.isTaskbarPresent -> {
return when {
isInSetup -> {
SetupNavLayoutter(
resources,
navButtonsView,
navButtonContainer,
endContextualContainer,
startContextualContainer,
imeSwitcher,
a11yButton,
space
)
}
isKidsMode -> {
KidsNavLayoutter(
resources,
@@ -146,7 +147,7 @@ class NavButtonLayoutFactory {
startContextualContainer,
imeSwitcher,
a11yButton,
space,
space
)
}
else ->
@@ -157,7 +158,7 @@ class NavButtonLayoutFactory {
startContextualContainer,
imeSwitcher,
a11yButton,
space,
space
)
}
}
@@ -194,7 +194,6 @@ public class NearestTouchFrame extends FrameLayout {
event.offsetLocation(mTouchingChild.getWidth() / 2 - x,
mTouchingChild.getHeight() / 2 - y);
return mTouchingChild.getVisibility() == VISIBLE
&& mTouchingChild.isAttachedToWindow()
&& mTouchingChild.dispatchTouchEvent(event);
}
}
@@ -17,21 +17,25 @@
package com.android.launcher3.taskbar.navbutton
import android.content.res.Resources
import android.view.Gravity
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Space
import com.android.launcher3.DeviceProfile
import com.android.launcher3.taskbar.TaskbarActivityContext
/** Layoutter for showing gesture navigation on phone screen. No buttons here, no-op container */
class PhoneGestureLayoutter(
resources: Resources,
navButtonsView: NearestTouchFrame,
navBarContainer: LinearLayout,
endContextualContainer: ViewGroup,
startContextualContainer: ViewGroup,
imeSwitcher: ImageView?,
a11yButton: ImageView?,
space: Space?,
space: Space?
) :
AbstractNavButtonLayoutter(
resources,
@@ -40,10 +44,33 @@ class PhoneGestureLayoutter(
startContextualContainer,
imeSwitcher,
a11yButton,
space,
space
) {
private val mNavButtonsView = navButtonsView
override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
// TODO: look into if we should use SetupNavLayoutter instead.
if (!context.isUserSetupComplete) {
// Since setup wizard only has back button enabled, it looks strange to be
// end-aligned, so start-align instead.
val navButtonsLayoutParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams
val navButtonsViewLayoutParams =
mNavButtonsView.layoutParams as FrameLayout.LayoutParams
val deviceProfile: DeviceProfile = context.deviceProfile
navButtonsLayoutParams.marginEnd = 0
navButtonsLayoutParams.gravity = Gravity.START
context.setTaskbarWindowSize(context.setupWindowSize)
adjustForSetupInPhoneMode(
navButtonsLayoutParams,
navButtonsViewLayoutParams,
deviceProfile
)
mNavButtonsView.layoutParams = navButtonsViewLayoutParams
navButtonContainer.layoutParams = navButtonsLayoutParams
}
endContextualContainer.removeAllViews()
startContextualContainer.removeAllViews()
}
@@ -17,7 +17,6 @@
package com.android.launcher3.taskbar.navbutton
import android.content.res.Resources
import android.os.SystemProperties
import android.view.Gravity
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
@@ -29,21 +28,18 @@ import com.android.launcher3.DeviceProfile
import com.android.launcher3.R
import com.android.launcher3.taskbar.TaskbarActivityContext
const val SUW_THEME_SYSTEM_PROPERTY = "setupwizard.theme"
const val GLIF_EXPRESSIVE_THEME = "glif_expressive"
const val GLIF_EXPRESSIVE_LIGHT_THEME = "glif_expressive_light"
const val SQUARE_ASPECT_RATIO_BOTTOM_BOUND = 0.95
const val SQUARE_ASPECT_RATIO_UPPER_BOUND = 1.05
class SetupNavLayoutter(
resources: Resources,
nearestTouchFrame: NearestTouchFrame,
navButtonsView: NearestTouchFrame,
navButtonContainer: LinearLayout,
endContextualContainer: ViewGroup,
startContextualContainer: ViewGroup,
imeSwitcher: ImageView?,
a11yButton: ImageView?,
space: Space?,
space: Space?
) :
AbstractNavButtonLayoutter(
resources,
@@ -52,21 +48,15 @@ class SetupNavLayoutter(
startContextualContainer,
imeSwitcher,
a11yButton,
space,
space
) {
// mNearestTouchFrame is a ViewGroup that contains start, end, nav button ViewGroups
private val mNearestTouchFrame = nearestTouchFrame
private val mNavButtonsView = navButtonsView
override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
val SUWTheme = SystemProperties.get(SUW_THEME_SYSTEM_PROPERTY, "")
if (SUWTheme == GLIF_EXPRESSIVE_THEME || SUWTheme == GLIF_EXPRESSIVE_LIGHT_THEME) {
return
}
// Since setup wizard only has back button enabled, it looks strange to be
// end-aligned, so start-align instead.
val navButtonsLayoutParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams
val navButtonsOverallViewGroupLayoutParams =
mNearestTouchFrame.layoutParams as FrameLayout.LayoutParams
val navButtonsViewLayoutParams = mNavButtonsView.layoutParams as FrameLayout.LayoutParams
val deviceProfile: DeviceProfile = context.deviceProfile
navButtonsLayoutParams.marginEnd = 0
@@ -82,14 +72,18 @@ class SetupNavLayoutter(
) {
navButtonsLayoutParams.marginStart =
resources.getDimensionPixelSize(R.dimen.taskbar_back_button_suw_start_margin)
navButtonsOverallViewGroupLayoutParams.bottomMargin =
navButtonsViewLayoutParams.bottomMargin =
resources.getDimensionPixelSize(R.dimen.taskbar_back_button_suw_bottom_margin)
navButtonsLayoutParams.height =
resources.getDimensionPixelSize(R.dimen.taskbar_back_button_suw_height)
} else {
adjustForSetupInPhoneMode(navButtonsOverallViewGroupLayoutParams, deviceProfile)
adjustForSetupInPhoneMode(
navButtonsLayoutParams,
navButtonsViewLayoutParams,
deviceProfile
)
}
mNearestTouchFrame.layoutParams = navButtonsOverallViewGroupLayoutParams
mNavButtonsView.layoutParams = navButtonsViewLayoutParams
navButtonContainer.layoutParams = navButtonsLayoutParams
endContextualContainer.removeAllViews()
@@ -103,7 +97,7 @@ class SetupNavLayoutter(
WRAP_CONTENT,
contextualMargin,
contextualMargin,
Gravity.START,
Gravity.START
)
if (imeSwitcher != null) {
@@ -18,11 +18,12 @@ package com.android.launcher3.taskbar.overlay;
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.taskbar.BaseTaskbarContext;
import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -31,7 +32,6 @@ import com.android.launcher3.taskbar.TaskbarDragController;
import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.taskbar.allapps.TaskbarAllAppsContainerView;
import com.android.launcher3.taskbar.allapps.TaskbarSearchSessionController;
import com.android.launcher3.util.NavigationMode;
import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource;
/**
@@ -57,7 +57,7 @@ public class TaskbarOverlayContext extends BaseTaskbarContext {
Context windowContext,
TaskbarActivityContext taskbarContext,
TaskbarControllers controllers) {
super(windowContext, taskbarContext.isPrimaryDisplay());
super(windowContext);
mTaskbarContext = taskbarContext;
mOverlayController = controllers.taskbarOverlayController;
mDragController = new TaskbarDragController(this);
@@ -66,12 +66,6 @@ public class TaskbarOverlayContext extends BaseTaskbarContext {
mStashedTaskbarHeight = controllers.taskbarStashController.getStashedHeight();
mUiController = controllers.uiController;
onViewCreated();
}
/** Called when the controller is destroyed. */
public void onDestroy() {
mDragController.onDestroy();
}
public @Nullable TaskbarSearchSessionController getSearchSessionController() {
@@ -125,6 +119,11 @@ public class TaskbarOverlayContext extends BaseTaskbarContext {
return mDragLayer.findViewById(R.id.apps_view);
}
@Override
public boolean isBindingItems() {
return mTaskbarContext.isBindingItems();
}
@Override
public View.OnClickListener getItemOnClickListener() {
return mTaskbarContext.getItemOnClickListener();
@@ -135,7 +134,6 @@ public class TaskbarOverlayContext extends BaseTaskbarContext {
return mDragController::startDragOnLongClick;
}
@NonNull
@Override
public PopupDataProvider getPopupDataProvider() {
return mTaskbarContext.getPopupDataProvider();
@@ -147,38 +145,8 @@ public class TaskbarOverlayContext extends BaseTaskbarContext {
}
@Override
public boolean isTransientTaskbar() {
return mTaskbarContext.isTransientTaskbar();
}
@Override
public boolean isPinnedTaskbar() {
return mTaskbarContext.isPinnedTaskbar();
}
@Override
public NavigationMode getNavigationMode() {
return mTaskbarContext.getNavigationMode();
}
@Override
public boolean isInDesktopMode() {
return mTaskbarContext.isInDesktopMode();
}
@Override
public boolean showLockedTaskbarOnHome() {
return mTaskbarContext.showLockedTaskbarOnHome();
}
@Override
public boolean showDesktopTaskbarForFreeformDisplay() {
return mTaskbarContext.showDesktopTaskbarForFreeformDisplay();
}
@Override
public boolean isPrimaryDisplay() {
return mTaskbarContext.isPrimaryDisplay();
public DotInfo getDotInfoForItem(ItemInfo info) {
return mTaskbarContext.getDotInfoForItem(info);
}
@Override
@@ -26,12 +26,8 @@ import static com.android.launcher3.LauncherState.ALL_APPS;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PixelFormat;
import android.util.Log;
import android.view.AttachedSurfaceControl;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.ViewRootImpl;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
@@ -39,11 +35,8 @@ import androidx.annotation.Nullable;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
import com.android.launcher3.R;
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.taskbar.TaskbarControllers;
import com.android.systemui.shared.system.BlurUtils;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -58,14 +51,12 @@ import java.util.Optional;
*/
public final class TaskbarOverlayController {
private static final String TAG = "TaskbarOverlayController";
private static final String WINDOW_TITLE = "Taskbar Overlay";
private final TaskbarActivityContext mTaskbarContext;
private final Context mWindowContext;
private final TaskbarOverlayProxyView mProxyView;
private final LayoutParams mLayoutParams;
private final int mMaxBlurRadius;
private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
@Override
@@ -96,8 +87,6 @@ public final class TaskbarOverlayController {
private DeviceProfile mLauncherDeviceProfile;
private @Nullable TaskbarOverlayContext mOverlayContext;
private TaskbarControllers mControllers; // Initialized in init.
// True if we have alerted surface flinger of an expensive call for blur.
private boolean mInEarlyWakeUp;
public TaskbarOverlayController(
TaskbarActivityContext taskbarContext, DeviceProfile launcherDeviceProfile) {
@@ -106,8 +95,6 @@ public final class TaskbarOverlayController {
mProxyView = new TaskbarOverlayProxyView();
mLayoutParams = createLayoutParams();
mLauncherDeviceProfile = launcherDeviceProfile;
mMaxBlurRadius = mTaskbarContext.getResources().getDimensionPixelSize(
R.dimen.max_depth_blur_radius_enhanced);
}
/** Initialize the controller. */
@@ -162,13 +149,9 @@ public final class TaskbarOverlayController {
/** Destroys the controller and any overlay window if present. */
public void onDestroy() {
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
Optional.ofNullable(mOverlayContext).ifPresent(c -> {
c.onDestroy();
WindowManager wm = c.getSystemService(WindowManager.class);
if (wm != null) {
wm.removeViewImmediate(mOverlayContext.getDragLayer());
}
});
Optional.ofNullable(mOverlayContext)
.map(c -> c.getSystemService(WindowManager.class))
.ifPresent(m -> m.removeViewImmediate(mOverlayContext.getDragLayer()));
mOverlayContext = null;
}
@@ -200,7 +183,7 @@ public final class TaskbarOverlayController {
private LayoutParams createLayoutParams() {
LayoutParams layoutParams = new LayoutParams(
TYPE_APPLICATION_OVERLAY,
/* flags = */ 0,
LayoutParams.FLAG_SPLIT_TOUCH,
PixelFormat.TRANSLUCENT);
layoutParams.setTitle(WINDOW_TITLE);
layoutParams.gravity = Gravity.BOTTOM;
@@ -212,73 +195,6 @@ public final class TaskbarOverlayController {
return layoutParams;
}
/**
* Sets the blur radius for the overlay window.
*
* @param radius the blur radius in pixels. This will automatically change to {@code 0} if blurs
* are unsupported on the device.
*/
public void setBackgroundBlurRadius(int radius) {
if (!Flags.allAppsBlur()) {
return;
}
if (!BlurUtils.supportsBlursOnWindows()) {
Log.d(TAG, "setBackgroundBlurRadius: not supported, setting to 0");
radius = 0;
// intentionally falling through in case a non-0 blur was previously set.
}
if (mOverlayContext == null) {
Log.w(TAG, "setBackgroundBlurRadius: no overlay context");
return;
}
TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer();
if (dragLayer == null) {
Log.w(TAG, "setBackgroundBlurRadius: no drag layer");
return;
}
ViewRootImpl dragLayerViewRoot = dragLayer.getViewRootImpl();
if (dragLayerViewRoot == null) {
Log.w(TAG, "setBackgroundBlurRadius: dragLayerViewRoot is null");
return;
}
AttachedSurfaceControl rootSurfaceControl = dragLayer.getRootSurfaceControl();
if (rootSurfaceControl == null) {
Log.w(TAG, "setBackgroundBlurRadius: rootSurfaceControl is null");
return;
}
SurfaceControl surfaceControl = dragLayerViewRoot.getSurfaceControl();
if (surfaceControl == null || !surfaceControl.isValid()) {
Log.w(TAG, "setBackgroundBlurRadius: surfaceControl is null or invalid");
return;
}
Log.v(TAG, "setBackgroundBlurRadius: " + radius);
SurfaceControl.Transaction transaction =
new SurfaceControl.Transaction().setBackgroundBlurRadius(surfaceControl, radius);
// Set early wake-up flags when we know we're executing an expensive operation, this way
// SurfaceFlinger will adjust its internal offsets to avoid jank.
boolean wantsEarlyWakeUp = radius > 0 && radius < mMaxBlurRadius;
if (wantsEarlyWakeUp && !mInEarlyWakeUp) {
Log.d(TAG, "setBackgroundBlurRadius: setting early wakeup");
try {
transaction.setEarlyWakeupStart();
} catch (NoSuchMethodError e) {
// LC-Ignored: wtf?
}
mInEarlyWakeUp = true;
} else if (!wantsEarlyWakeUp && mInEarlyWakeUp) {
Log.d(TAG, "setBackgroundBlurRadius: clearing early wakeup");
try {
transaction.setEarlyWakeupEnd();
} catch (NoSuchMethodError e) {
// LC-Ignored: wtf?
}
mInEarlyWakeUp = false;
}
rootSurfaceControl.applyTransactionOnDraw(transaction);
}
/**
* Proxy view connecting taskbar drag layer to the overlay window.
*
@@ -300,13 +216,6 @@ public final class TaskbarOverlayController {
@Override
protected void handleClose(boolean animate) {
if (!mIsOpen) return;
if (Flags.taskbarOverflow()) {
// Mark the view closed before attempting to remove it, so the drag layer does not
// schedule another call to close. Needed for taskbar overflow in case the KQS
// view shown for taskbar overflow needs to be reshown - delayed close call would
// would result in reshown KQS view getting hidden.
mIsOpen = false;
}
mTaskbarContext.getDragLayer().removeView(this);
Optional.ofNullable(mOverlayContext).ifPresent(c -> {
if (canCloseWindow()) {
@@ -34,6 +34,7 @@ import com.android.app.viewcapture.ViewCaptureFactory;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
@@ -72,7 +73,7 @@ public class TaskbarOverlayDragLayer extends
@Override
public void recreateControllers() {
List<TouchController> controllers = new ArrayList<>();
controllers.add(mContainer.getDragController());
controllers.add(mActivity.getDragController());
controllers.addAll(mTouchControllers);
mControllers = controllers.toArray(new TouchController[0]);
}
@@ -86,7 +87,7 @@ public class TaskbarOverlayDragLayer extends
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK) {
AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer);
AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
if (topView != null && topView.canHandleBack()) {
topView.onBackInvoked();
return true;
@@ -95,7 +96,7 @@ public class TaskbarOverlayDragLayer extends
&& event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE && event.hasNoModifiers()) {
// Ignore escape if pressed in conjunction with any modifier keys. Close each
// floating view one at a time for each key press.
AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mContainer);
AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
if (topView != null) {
topView.close(/* animate= */ true);
return true;
@@ -106,7 +107,7 @@ public class TaskbarOverlayDragLayer extends
@Override
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
if (mContainer.isAnySystemDragInProgress()) {
if (mActivity.isAnySystemDragInProgress()) {
inoutInfo.touchableRegion.setEmpty();
inoutInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
}
@@ -122,7 +123,7 @@ public class TaskbarOverlayDragLayer extends
@Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
mContainer.getOverlayController().maybeCloseWindow();
mActivity.getOverlayController().maybeCloseWindow();
}
/** Adds a {@link TouchController} to this drag layer. */
@@ -146,14 +147,14 @@ public class TaskbarOverlayDragLayer extends
* 2) Sets tappableInsets bottom inset to 0.
*/
private WindowInsets updateInsetsDueToStashing(WindowInsets oldInsets) {
if (!mContainer.isTransientTaskbar()) {
if (!DisplayController.isTransientTaskbar(mActivity)) {
return oldInsets;
}
WindowInsets.Builder updatedInsetsBuilder = new WindowInsets.Builder(oldInsets);
Insets oldNavInsets = oldInsets.getInsets(WindowInsets.Type.navigationBars());
Insets newNavInsets = Insets.of(oldNavInsets.left, oldNavInsets.top, oldNavInsets.right,
mContainer.getStashedTaskbarHeight());
mActivity.getStashedTaskbarHeight());
updatedInsetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets);
Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement());
@@ -0,0 +1,161 @@
/*
* Copyright (C) 2019 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.uioverrides;
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
import static com.android.app.animation.Interpolators.AGGRESSIVE_EASE_IN_OUT;
import static com.android.app.animation.Interpolators.FINAL_FRAME;
import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.launcher3.LauncherState.QUICK_SWITCH_FROM_HOME;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
import static com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA;
import android.util.FloatProperty;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import com.android.launcher3.LauncherState;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.quickstep.views.RecentsView;
/**
* State handler for recents view. Manages UI changes and animations for recents view based off the
* current {@link LauncherState}.
*
* @param <T> the recents view
*/
public abstract class BaseRecentsViewStateController<T extends RecentsView>
implements StateHandler<LauncherState> {
protected final T mRecentsView;
protected final QuickstepLauncher mLauncher;
public BaseRecentsViewStateController(@NonNull QuickstepLauncher launcher) {
mLauncher = launcher;
mRecentsView = launcher.getOverviewPanel();
}
@Override
public void setState(@NonNull LauncherState state) {
float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
RECENTS_SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
ADJACENT_PAGE_HORIZONTAL_OFFSET.set(mRecentsView, scaleAndOffset[1]);
TASK_SECONDARY_TRANSLATION.set(mRecentsView, 0f);
getContentAlphaProperty().set(mRecentsView, state.isRecentsViewVisible ? 1f : 0);
getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness());
RECENTS_GRID_PROGRESS.set(mRecentsView,
state.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f);
TASK_THUMBNAIL_SPLASH_ALPHA.set(mRecentsView, state.showTaskThumbnailSplash() ? 1f : 0f);
}
@Override
public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
PendingAnimation builder) {
if (config.hasAnimationFlag(SKIP_OVERVIEW)) {
return;
}
setStateWithAnimationInternal(toState, config, builder);
builder.addEndListener(success -> {
if (!success) {
mRecentsView.reset();
}
});
}
/**
* Core logic for animating the recents view UI.
*
* @param toState state to animate to
* @param config current animation config
* @param setter animator set builder
*/
void setStateWithAnimationInternal(@NonNull final LauncherState toState,
@NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
setter.setFloat(mRecentsView, ADJACENT_PAGE_HORIZONTAL_OFFSET, scaleAndOffset[1],
config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
setter.setFloat(mRecentsView, TASK_SECONDARY_TRANSLATION, 0f,
config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
boolean exitingOverview =
!FeatureFlags.enableSplitContextually() && !toState.isRecentsViewVisible;
if (mRecentsView.isSplitSelectionActive() && exitingOverview) {
setter.add(mRecentsView.getSplitSelectController().getSplitAnimationController()
.createPlaceholderDismissAnim(mLauncher, LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
setter.getDuration()));
setter.setViewAlpha(
mRecentsView.getSplitInstructionsView(),
0,
config.getInterpolator(
ANIM_OVERVIEW_SPLIT_SELECT_INSTRUCTIONS_FADE,
LINEAR
)
);
}
setter.setFloat(mRecentsView, getContentAlphaProperty(),
toState.isRecentsViewVisible ? 1 : 0,
config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
setter.setFloat(
mRecentsView, getTaskModalnessProperty(),
toState.getOverviewModalness(),
config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
LauncherState fromState = mLauncher.getStateManager().getState();
setter.setFloat(mRecentsView, TASK_THUMBNAIL_SPLASH_ALPHA,
toState.showTaskThumbnailSplash() ? 1f : 0f,
getOverviewInterpolator(fromState, toState));
setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS,
toState.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f,
getOverviewInterpolator(fromState, toState));
}
private Interpolator getOverviewInterpolator(LauncherState fromState, LauncherState toState) {
return fromState == QUICK_SWITCH_FROM_HOME
? ACCELERATE_DECELERATE
: toState.isRecentsViewVisible ? INSTANT : FINAL_FRAME;
}
abstract FloatProperty getTaskModalnessProperty();
/**
* Get property for content alpha for the recents view.
*
* @return the float property for the view's content alpha
*/
abstract FloatProperty getContentAlphaProperty();
}
@@ -16,16 +16,17 @@
package com.android.launcher3.uioverrides;
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.Keyframe;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
@@ -35,25 +36,24 @@ import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Process;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Log;
import android.util.Property;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.core.graphics.ColorUtils;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.DelegatedCellDrawing;
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -62,6 +62,10 @@ import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.DoubleShadowBubbleTextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import app.lawnchair.theme.color.tokens.ColorTokens;
/**
@@ -69,26 +73,12 @@ import app.lawnchair.theme.color.tokens.ColorTokens;
*/
public class PredictedAppIcon extends DoubleShadowBubbleTextView {
private static final float RING_SCALE_START_VALUE = 0.75f;
private static final int RING_SHADOW_COLOR = 0x99000000;
private static final float RING_EFFECT_RATIO = Flags.enableLauncherIconShapes() ? 0.1f : 0.095f;
private static final float RING_EFFECT_RATIO = 0.095f;
private static final long ICON_CHANGE_ANIM_DURATION = 360;
private static final long ICON_CHANGE_ANIM_STAGGER = 50;
private static final Property<PredictedAppIcon, Float> RING_SCALE_PROPERTY =
new Property<>(Float.TYPE, "ringScale") {
@Override
public Float get(PredictedAppIcon icon) {
return icon.mRingScale;
}
@Override
public void set(PredictedAppIcon icon, Float value) {
icon.mRingScale = value;
icon.invalidate();
}
};
boolean mIsDrawingDot = false;
private final DeviceProfile mDeviceProfile;
private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
@@ -100,23 +90,16 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView {
private final BlurMaskFilter mShadowFilter;
private boolean mIsPinned = false;
private final AnimColorHolder mPlateColor = new AnimColorHolder();
private int mPlateColor;
boolean mDrawForDrag = false;
// Used for the "slot-machine" animation when prediction changes.
private final Rect mSlotIconBound = new Rect(0, 0, getIconSize(), getIconSize());
private Drawable mSlotMachineIcon;
// Used for the "slot-machine" education animation.
private List<Drawable> mSlotMachineIcons;
private Animator mSlotMachineAnim;
private float mSlotMachineIconTranslationY;
// Used to animate the "ring" around predicted icons
private float mRingScale = 1f;
private boolean mForceHideRing = false;
private Animator mRingScaleAnim;
private int mWidth;
private static final FloatProperty<PredictedAppIcon> SLOT_MACHINE_TRANSLATION_Y =
new FloatProperty<PredictedAppIcon>("slotMachineTranslationY") {
private static final FloatProperty<PredictedAppIcon> SLOT_MACHINE_TRANSLATION_Y = new FloatProperty<PredictedAppIcon>(
"slotMachineTranslationY") {
@Override
public void setValue(PredictedAppIcon predictedAppIcon, float transY) {
predictedAppIcon.mSlotMachineIconTranslationY = transY;
@@ -137,39 +120,50 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView {
this(context, attrs, 0);
}
Context mContext;
public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile();
mNormalizedIconSize = Math.round(getIconSize() * ICON_VISIBLE_AREA_FACTOR);
mNormalizedIconSize = IconNormalizer.getNormalizedCircleSize(getIconSize());
int shadowSize = context.getResources().getDimensionPixelSize(
R.dimen.blur_size_thin_outline);
mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
mShapePath = ThemeManager.INSTANCE.get(context).getIconShape().getPath(mNormalizedIconSize);
mShapePath = GraphicsUtils.getShapePath(context, mNormalizedIconSize);
}
@Override
public void onDraw(Canvas canvas) {
int count = canvas.save();
boolean isSlotMachineAnimRunning = mSlotMachineIcon != null;
boolean isSlotMachineAnimRunning = mSlotMachineAnim != null;
if (!mIsPinned) {
drawRingEffect(canvas);
drawEffect(canvas);
if (isSlotMachineAnimRunning) {
// Clip to to outside of the ring during the slot machine animation.
canvas.clipPath(mRingPath);
}
canvas.scale(1 - 2f * RING_EFFECT_RATIO, 1 - 2f * RING_EFFECT_RATIO,
getWidth() * .5f, getHeight() * .5f);
if (isSlotMachineAnimRunning) {
canvas.translate(0, mSlotMachineIconTranslationY);
mSlotMachineIcon.setBounds(mSlotIconBound);
mSlotMachineIcon.draw(canvas);
canvas.translate(0, getSlotMachineIconPlusSpacingSize());
}
canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
}
if (isSlotMachineAnimRunning) {
drawSlotMachineIcons(canvas);
} else {
super.onDraw(canvas);
}
super.onDraw(canvas);
canvas.restoreToCount(count);
}
private void drawSlotMachineIcons(Canvas canvas) {
canvas.translate((getWidth() - getIconSize()) / 2f,
(getHeight() - getIconSize()) / 2f + mSlotMachineIconTranslationY);
for (Drawable icon : mSlotMachineIcons) {
icon.setBounds(0, 0, getIconSize(), getIconSize());
icon.draw(canvas);
canvas.translate(0, getSlotMachineIconPlusSpacingSize());
}
}
private float getSlotMachineIconPlusSpacingSize() {
return getIconSize() + getOutlineOffsetY();
}
@@ -185,95 +179,118 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView {
mIsDrawingDot = false;
}
/**
* Returns whether the newInfo differs from the current getTag().
*/
private boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) {
boolean changedIcons = getTag() instanceof WorkspaceItemInfo oldInfo
&& oldInfo.getTargetComponent() != null
&& newInfo.getTargetComponent() != null
&& !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent());
return changedIcons && isShown();
}
@Override
public void applyIconAndLabel(ItemInfoWithIcon info) {
super.applyIconAndLabel(info);
public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
// Create the slot machine animation first, since it uses the current icon to
// start.
Animator slotMachineAnim = animate
? createSlotMachineAnim(Collections.singletonList(info.bitmap), false)
: null;
super.applyFromWorkspaceItem(info, animate, staggerIndex);
int oldPlateColor = mPlateColor;
int newPlateColor;
if (getIcon().isThemed()) {
mPlateColor.endColor = ColorTokens.PredictedPlateColor.resolveColor(getContext());
newPlateColor = ColorTokens.PredictedPlateColor.resolveColor(getContext());
} else {
float[] hctPlateColor = new float[3];
ColorUtils.colorToM3HCT(mDotParams.appColor, hctPlateColor);
mPlateColor.endColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85);
newPlateColor = ColorUtils.M3HCTToColor(hctPlateColor[0], 36, 85);
}
mPlateColor.onUpdate();
}
/**
* Tries to apply the icon with animation and returns true if the icon was indeed animated
*/
public boolean applyFromWorkspaceItemWithAnimation(WorkspaceItemInfo info, int staggerIndex) {
boolean animate = shouldAnimateIconChange(info);
Drawable oldIcon = getIcon();
int oldPlateColor = mPlateColor.currentColor;
applyFromWorkspaceItem(info);
setContentDescription(
mIsPinned ? info.contentDescription :
getContext().getString(R.string.hotseat_prediction_content_description,
info.contentDescription));
if (!animate) {
mPlateColor.startColor = mPlateColor.endColor;
mPlateColor.progress.value = 1;
mPlateColor.onUpdate();
mPlateColor = newPlateColor;
}
if (mIsPinned) {
setContentDescription(info.contentDescription);
} else {
mPlateColor.startColor = oldPlateColor;
mPlateColor.progress.value = 0;
mPlateColor.onUpdate();
setContentDescription(
getContext().getString(R.string.hotseat_prediction_content_description,
info.contentDescription));
}
if (animate) {
ValueAnimator plateColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(),
oldPlateColor, newPlateColor);
plateColorAnim.addUpdateListener(valueAnimator -> {
mPlateColor = (int) valueAnimator.getAnimatedValue();
invalidate();
});
AnimatorSet changeIconAnim = new AnimatorSet();
ObjectAnimator plateColorAnim =
ObjectAnimator.ofFloat(mPlateColor.progress, AnimatedFloat.VALUE, 0, 1);
plateColorAnim.setAutoCancel(true);
changeIconAnim.play(plateColorAnim);
if (!mIsPinned && oldIcon != null) {
// Play the slot machine icon
mSlotMachineIcon = oldIcon;
float finalTrans = -getSlotMachineIconPlusSpacingSize();
Keyframe[] keyframes = new Keyframe[] {
Keyframe.ofFloat(0f, 0f),
Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot
Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position
};
keyframes[1].setInterpolator(ACCELERATE_DECELERATE);
keyframes[2].setInterpolator(ACCELERATE_DECELERATE);
ObjectAnimator slotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this,
PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes));
slotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> {
mSlotMachineIcon = null;
mSlotMachineIconTranslationY = 0;
invalidate();
}));
slotMachineAnim.setAutoCancel(true);
if (slotMachineAnim != null) {
changeIconAnim.play(slotMachineAnim);
}
changeIconAnim.play(plateColorAnim);
changeIconAnim.setStartDelay(staggerIndex * ICON_CHANGE_ANIM_STAGGER);
changeIconAnim.setDuration(ICON_CHANGE_ANIM_DURATION).start();
}
return animate;
}
/**
* Returns an Animator that translates the given icons in a "slot-machine"
* fashion, beginning
* and ending with the original icon.
*/
public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate) {
return createSlotMachineAnim(iconsToAnimate, true);
}
/**
* Returns an Animator that translates the given icons in a "slot-machine"
* fashion, beginning
* with the original icon, then cycling through the given icons, optionally
* ending back with
* the original icon.
*
* @param endWithOriginalIcon Whether we should land back on the icon we started
* with, rather
* than the last item in iconsToAnimate.
*/
public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate,
boolean endWithOriginalIcon) {
if (mIsPinned || iconsToAnimate == null || iconsToAnimate.isEmpty()) {
return null;
}
if (mSlotMachineAnim != null) {
mSlotMachineAnim.end();
}
// Bookend the other animating icons with the original icon on both ends.
mSlotMachineIcons = new ArrayList<>(iconsToAnimate.size() + 2);
mSlotMachineIcons.add(getIcon());
iconsToAnimate.stream()
.map(iconInfo -> iconInfo.newIcon(mContext))
.forEach(mSlotMachineIcons::add);
if (endWithOriginalIcon) {
mSlotMachineIcons.add(getIcon());
}
float finalTrans = -getSlotMachineIconPlusSpacingSize() * (mSlotMachineIcons.size() - 1);
Keyframe[] keyframes = new Keyframe[] {
Keyframe.ofFloat(0f, 0f),
Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot
Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position
};
keyframes[1].setInterpolator(ACCELERATE_DECELERATE);
keyframes[2].setInterpolator(ACCELERATE_DECELERATE);
mSlotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this,
PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes));
mSlotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> {
mSlotMachineIcons = null;
mSlotMachineAnim = null;
mSlotMachineIconTranslationY = 0;
invalidate();
}));
return mSlotMachineAnim;
}
/**
* Removes prediction ring from app icon
*/
public void pin(WorkspaceItemInfo info) {
if (mIsPinned) return;
if (mIsPinned)
return;
mIsPinned = true;
applyFromWorkspaceItem(info);
setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
@@ -305,13 +322,7 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView {
}
private int getOutlineOffsetX() {
int measuredWidth = getMeasuredWidth();
if (mDisplay != DISPLAY_TASKBAR) {
Log.d("b/387844520", "getOutlineOffsetX: measured width = " + measuredWidth
+ ", mNormalizedIconSize = " + mNormalizedIconSize
+ ", last updated width = " + mWidth);
}
return (mWidth - mNormalizedIconSize) / 2;
return (getMeasuredWidth() - mNormalizedIconSize) / 2;
}
private int getOutlineOffsetY() {
@@ -324,11 +335,6 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView {
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mSlotIconBound.offsetTo((w - getIconSize()) / 2, (h - getIconSize()) / 2);
if (mDisplay != DISPLAY_TASKBAR) {
Log.d("b/387844520", "calling updateRingPath from onSizeChanged");
}
updateRingPath();
}
@@ -339,13 +345,18 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView {
}
private void updateRingPath() {
mRingPath.reset();
mTmpMatrix.reset();
mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY());
mRingPath.addPath(mShapePath, mTmpMatrix);
boolean isBadged = false;
if (getTag() instanceof WorkspaceItemInfo) {
WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
isBadged = !Process.myUserHandle().equals(info.user)
|| info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
}
FastBitmapDrawable icon = getIcon();
if (icon != null && icon.getBadge() != null) {
mRingPath.reset();
mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY());
mRingPath.addPath(mShapePath, mTmpMatrix);
if (isBadged) {
float outlineSize = mNormalizedIconSize * RING_EFFECT_RATIO;
float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO);
float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize;
@@ -355,73 +366,19 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView {
mTmpMatrix.preTranslate(-mNormalizedIconSize, -mNormalizedIconSize);
mRingPath.addPath(mShapePath, mTmpMatrix);
}
invalidate();
}
@Override
public void setForceHideRing(boolean forceHideRing) {
if (mForceHideRing == forceHideRing) {
return;
}
mForceHideRing = forceHideRing;
if (forceHideRing) {
invalidate();
} else {
animateRingScale(RING_SCALE_START_VALUE, 1);
}
}
private void cancelRingScaleAnim() {
if (mRingScaleAnim != null) {
mRingScaleAnim.cancel();
}
}
private void animateRingScale(float... ringScale) {
cancelRingScaleAnim();
mRingScaleAnim = ObjectAnimator.ofFloat(this, RING_SCALE_PROPERTY, ringScale);
mRingScaleAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mRingScaleAnim = null;
}
});
mRingScaleAnim.start();
}
private void drawRingEffect(Canvas canvas) {
// Don't draw ring effect if item is about to be dragged or if the icon is not visible.
if (mDrawForDrag || !mIsIconVisible || mForceHideRing) {
private void drawEffect(Canvas canvas) {
// Don't draw ring effect if item is about to be dragged.
if (mDrawForDrag) {
return;
}
mIconRingPaint.setColor(RING_SHADOW_COLOR);
mIconRingPaint.setMaskFilter(mShadowFilter);
int count = canvas.save();
if (Flags.enableLauncherIconShapes()) {
// Scale canvas properly to for ring to be inner stroke and not exceed bounds.
// Since STROKE draws half on either side of Path, scale canvas down by 1x stroke ratio.
canvas.scale(
mRingScale * (1f - RING_EFFECT_RATIO),
mRingScale * (1f - RING_EFFECT_RATIO),
getWidth() / 2f,
getHeight() / 2f);
} else if (Float.compare(1, mRingScale) != 0) {
canvas.scale(mRingScale, mRingScale, getWidth() / 2f, getHeight() / 2f);
}
// Draw ring shadow around canvas.
canvas.drawPath(mRingPath, mIconRingPaint);
mIconRingPaint.setColor(mPlateColor.currentColor);
if (Flags.enableLauncherIconShapes()) {
mIconRingPaint.setStrokeWidth(getWidth() * RING_EFFECT_RATIO);
// Using FILL_AND_STROKE as there is still some gap to fill,
// between inner curve of ring / outer curve of icon.
mIconRingPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
mIconRingPaint.setColor(mPlateColor);
mIconRingPaint.setMaskFilter(null);
// Draw ring around canvas.
canvas.drawPath(mRingPath, mIconRingPaint);
canvas.restoreToCount(count);
}
@Override
@@ -470,21 +427,6 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView {
return icon;
}
private class AnimColorHolder {
public final AnimatedFloat progress = new AnimatedFloat(this::onUpdate, 1);
public final ArgbEvaluator evaluator = ArgbEvaluator.getInstance();
public Integer startColor = 0;
public Integer endColor = 0;
public int currentColor = 0;
private void onUpdate() {
currentColor = (Integer) evaluator.evaluate(progress.value, startColor, endColor);
invalidate();
}
}
/**
* Draws Predicted Icon outline on cell layout
*/
@@ -0,0 +1,74 @@
/**
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.uioverrides;
import static com.android.launcher3.widget.LauncherWidgetHolder.APPWIDGET_HOST_ID;
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.os.Looper;
import androidx.annotation.NonNull;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.util.Executors;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.LauncherWidgetHolder;
import java.util.function.IntConsumer;
/**
* {@link AppWidgetHost} that is used to receive the changes to the widgets without
* storing any {@code Activity} info like that of the launcher.
*/
final class QuickstepAppWidgetHost extends AppWidgetHost {
private final @NonNull Context mContext;
private final @NonNull IntConsumer mAppWidgetRemovedCallback;
private final @NonNull LauncherWidgetHolder.ProviderChangedListener mProvidersChangedListener;
QuickstepAppWidgetHost(@NonNull Context context, @NonNull IntConsumer appWidgetRemovedCallback,
@NonNull LauncherWidgetHolder.ProviderChangedListener listener,
@NonNull Looper looper) {
super(context, APPWIDGET_HOST_ID, null, looper);
mContext = context;
mAppWidgetRemovedCallback = appWidgetRemovedCallback;
mProvidersChangedListener = listener;
}
@Override
protected void onProvidersChanged() {
mProvidersChangedListener.notifyWidgetProvidersChanged();
}
@Override
public void onAppWidgetRemoved(int appWidgetId) {
// Route the call via model thread, in case it comes while a loader-bind is in progress
Executors.MODEL_EXECUTOR.execute(
() -> Executors.MAIN_EXECUTOR.execute(
() -> mAppWidgetRemovedCallback.accept(appWidgetId)));
}
@Override
protected void onProviderChanged(int appWidgetId, @NonNull AppWidgetProviderInfo appWidget) {
LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo.fromProviderInfo(
mContext, appWidget);
super.onProviderChanged(appWidgetId, info);
// The super method updates the dimensions of the providerInfo. Update the
// launcher spans accordingly.
info.initSpans(mContext, LauncherAppState.getIDP(mContext));
}
}
@@ -23,6 +23,7 @@ import android.app.ActivityTaskManager;
import android.app.IActivityTaskManagerHidden;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.util.Pair;
@@ -30,19 +31,20 @@ import android.view.View;
import android.widget.RemoteViews;
import android.window.SplashScreen;
import com.android.launcher3.Utilities;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.ActivityOptionsWrapper;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
import java.util.function.Consumer;
import dev.rikka.tools.refine.Refine;
import app.lawnchair.LawnchairApp;
import dev.rikka.tools.refine.Refine;
/** Provides a Quickstep specific animation when launching an activity from an app widget. */
class QuickstepInteractionHandler implements RemoteViews.InteractionHandler,
Consumer<LauncherAppWidgetHostView> {
/**
* Provides a Quickstep specific animation when launching an activity from an
* app widget.
*/
public class QuickstepInteractionHandler implements RemoteViews.InteractionHandler {
private static final String TAG = "QuickstepInteractionHandler";
@@ -52,15 +54,10 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler,
mLauncher = launcher;
}
@Override
public void accept(LauncherAppWidgetHostView host) {
host.setInteractionHandler(this);
}
@SuppressWarnings("NewApi")
@Override
public boolean onInteraction(View view, PendingIntent pendingIntent,
RemoteViews.RemoteResponse remoteResponse) {
RemoteViews.RemoteResponse remoteResponse) {
LauncherAppWidgetHostView hostView = findHostViewAncestor(view);
if (hostView == null) {
Log.e(TAG, "View did not have a LauncherAppWidgetHostView ancestor.");
@@ -75,14 +72,8 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler,
return true;
}
Pair<Intent, ActivityOptions> options = remoteResponse.getLaunchOptions(view);
ActivityOptionsWrapper activityOptions = null;
try {
activityOptions = mLauncher.getAppTransitionManager()
.getActivityLaunchOptions(hostView, (ItemInfo) hostView.getTag());
} catch (NullPointerException e) {
Log.e("pE(C7evQZDJ)", "Failed to get activity launch options");
}
ActivityOptionsWrapper activityOptions = mLauncher.getAppTransitionManager()
.getActivityLaunchOptions(hostView);
if (!pendingIntent.isActivity()) {
// In the event this pending intent eventually launches an activity, i.e. a trampoline,
// use the Quickstep transition animation.
@@ -98,8 +89,7 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler,
pendingIntent.getCreatorPackage(),
activityOptions.options.getRemoteAnimationAdapter());
}
} catch (NullPointerException | RemoteException e) {
// pE-TODO(C7evQZDJ): Remove NullPointerException after fixing
} catch (RemoteException e) {
// Do nothing.
}
}
@@ -111,24 +101,16 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler,
} catch (Throwable t) {
// ignore
}
// pE-TODO(C7evQZDJ): Remove activityOptions null check
if (activityOptions != null) {
options = Pair.create(options.first, activityOptions.options);
}
options = Pair.create(options.first, activityOptions.options);
if (pendingIntent.isActivity()) {
logAppLaunch(hostView.getTag());
}
if (activityOptions != null) {
return RemoteViews.startPendingIntent(hostView, pendingIntent, options);
} else {
Log.d("pE(C7evQZDJ)", "activityOptions is null!");
return RemoteViews.startPendingIntent(hostView, pendingIntent,
remoteResponse.getLaunchOptions(view));
}
return RemoteViews.startPendingIntent(hostView, pendingIntent, options);
}
/**
* Logs that the app was launched from the widget.
*
* @param itemInfo the widget info.
*/
private void logAppLaunch(Object itemInfo) {
@@ -141,7 +123,8 @@ class QuickstepInteractionHandler implements RemoteViews.InteractionHandler,
private LauncherAppWidgetHostView findHostViewAncestor(View v) {
while (v != null) {
if (v instanceof LauncherAppWidgetHostView) return (LauncherAppWidgetHostView) v;
if (v instanceof LauncherAppWidgetHostView)
return (LauncherAppWidgetHostView) v;
v = (View) v.getParent();
}
return null;
@@ -16,13 +16,11 @@
package com.android.launcher3.uioverrides;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE;
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.internal.jank.Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
import static com.android.launcher3.Flags.enableExpressiveDismissTaskMotion;
import static com.android.launcher3.Flags.enablePredictiveBackGesture;
import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.PENDING_SPLIT_SELECT_INFO;
import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE;
@@ -37,17 +35,13 @@ import static com.android.launcher3.LauncherState.NO_OFFSET;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
import static com.android.launcher3.Utilities.ATLEAST_BAKLAVA;
import static com.android.launcher3.Utilities.ATLEAST_S_V2;
import static com.android.launcher3.Utilities.ATLEAST_T;
import static com.android.launcher3.Utilities.isRtl;
import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED;
import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition;
import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
import static com.android.launcher3.popup.SystemShortcut.BUBBLE_SHORTCUT;
import static com.android.launcher3.popup.SystemShortcut.DONT_SUGGEST_APP;
import static com.android.launcher3.popup.SystemShortcut.INSTALL;
import static com.android.launcher3.popup.SystemShortcut.PRIVATE_PROFILE_INSTALL;
@@ -66,6 +60,9 @@ 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.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
import static com.android.window.flags2.Flags.enableDesktopWindowingMode;
import static com.android.window.flags2.Flags.enableDesktopWindowingWallpaperActivity;
import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -74,27 +71,23 @@ import android.app.ActivityOptions;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.ShortcutInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.display.DisplayManager;
import android.media.permission.SafeCloseable;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.Looper;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Display;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.View;
import android.widget.AnalogClock;
import android.widget.TextClock;
import android.window.BackEvent;
import android.window.OnBackAnimationCallback;
import android.window.OnBackInvokedDispatcher;
@@ -104,7 +97,6 @@ import android.window.SplashScreen;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.app.viewcapture.ViewCaptureFactory;
import com.android.launcher3.AbstractFloatingView;
@@ -114,13 +106,13 @@ import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.LauncherState;
import com.android.launcher3.OnBackPressedHandler;
import com.android.launcher3.QuickstepAccessibilityDelegate;
import com.android.launcher3.QuickstepTransitionManager;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.allapps.AllAppsRecyclerView;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.apppairs.AppPairIcon;
@@ -142,9 +134,9 @@ import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.taskbar.LauncherTaskbarUIController;
import com.android.launcher3.taskbar.TaskbarManager;
import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.uioverrides.QuickstepWidgetHolder.QuickstepHolderFactory;
import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory;
import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
import com.android.launcher3.uioverrides.touchcontrollers.NoButtonNavbarToOverviewTouchController;
@@ -152,10 +144,7 @@ import com.android.launcher3.uioverrides.touchcontrollers.NoButtonQuickSwitchTou
import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
import com.android.launcher3.uioverrides.touchcontrollers.QuickSwitchTouchController;
import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController;
import com.android.launcher3.uioverrides.touchcontrollers.TaskViewDismissTouchController;
import com.android.launcher3.uioverrides.touchcontrollers.TaskViewLaunchTouchController;
import com.android.launcher3.uioverrides.touchcontrollers.TaskViewRecentsTouchContext;
import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchControllerDeprecated;
import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController;
import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController;
import com.android.launcher3.uioverrides.touchcontrollers.TwoButtonNavbarTouchController;
import com.android.launcher3.util.ActivityOptionsWrapper;
@@ -169,23 +158,20 @@ import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource;
import com.android.launcher3.util.StableViewInfo;
import com.android.launcher3.util.StartActivityParams;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.FloatingIconView;
import com.android.launcher3.widget.LauncherWidgetHolder;
import com.android.quickstep.OverviewCommandHelper;
import com.android.quickstep.OverviewComponentObserver;
import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
import com.android.quickstep.RecentsAnimationDeviceState;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskUtils;
import com.android.quickstep.TouchInteractionService.TISBinder;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.quickstep.util.AsyncClockEventDelegate;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LauncherUnfoldAnimationController;
import com.android.quickstep.util.QuickstepOnboardingPrefs;
import com.android.quickstep.util.SplitSelectStateController;
import com.android.quickstep.util.SplitTask;
import com.android.quickstep.util.SplitToWorkspaceController;
import com.android.quickstep.util.SplitWithKeyboardShortcutController;
import com.android.quickstep.util.TISBindHelper;
@@ -196,7 +182,6 @@ import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.RecentsViewContainer;
import com.android.quickstep.views.TaskView;
import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.unfold.RemoteUnfoldSharedComponent;
import com.android.systemui.unfold.UnfoldTransitionFactory;
@@ -206,10 +191,9 @@ import com.android.systemui.unfold.config.UnfoldTransitionConfig;
import com.android.systemui.unfold.dagger.UnfoldMain;
import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver;
import com.android.systemui.unfold.updates.RotationChangeProvider;
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import app.lawnchair.LawnchairApp;
import app.lawnchair.compat.LawnchairQuickstepCompat;
import kotlin.Unit;
import java.io.FileDescriptor;
@@ -224,21 +208,19 @@ import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import app.lawnchair.LawnchairApp;
import app.lawnchair.compat.LawnchairQuickstepCompat;
public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
SystemShortcut.BubbleActivityStarter {
public class QuickstepLauncher extends Launcher implements RecentsViewContainer {
private static final boolean TRACE_LAYOUTS =
SystemProperties.getBoolean("persist.debug.trace_layouts", false);
private static final String TRACE_RELAYOUT_CLASS =
SystemProperties.get("persist.debug.trace_request_layout_class", null);
public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
protected static final String RING_APPEAR_ANIMATION_PREFIX = "RingAppearAnimation\t";
private FixedContainerItems mAllAppsPredictions;
private HotseatPredictionController mHotseatPredictionController;
private DepthController mDepthController;
private @Nullable DesktopVisibilityController mDesktopVisibilityController;
private QuickstepTransitionManager mAppTransitionManager;
private OverviewActionsView<?> mActionsView;
@@ -251,7 +233,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
private SplitSelectStateController mSplitSelectStateController;
private SplitWithKeyboardShortcutController mSplitWithKeyboardShortcutController;
private SplitToWorkspaceController mSplitToWorkspaceController;
private BubbleBarLocation mBubbleBarLocation;
/**
* If Launcher restarted while in the middle of an Overview split select, it needs this data to
@@ -268,57 +249,35 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
private boolean mIsPredictiveBackToHomeInProgress;
private boolean mCanShowAllAppsEducationView;
private boolean mIsOverlayVisible;
private final OverviewChangeListener mOverviewChangeListener = this::onOverviewTargetChanged;
private final TaskViewRecentsTouchContext mTaskViewRecentsTouchContext =
new TaskViewRecentsTouchContext() {
@Override
public boolean isRecentsInteractive() {
return isInState(OVERVIEW) || isInState(OVERVIEW_MODAL_TASK);
}
@Override
public boolean isRecentsModal() {
return isInState(OVERVIEW_MODAL_TASK);
}
@Override
public void onUserControlledAnimationCreated(
AnimatorPlaybackController animController) {
getStateManager().setCurrentUserControlledAnimation(animController);
}
};
public static QuickstepLauncher getLauncher(Context context) {
return fromContext(context);
}
@Override
protected void setupViews() {
getAppWidgetHolder().setOnViewCreationCallback(new QuickstepInteractionHandler(this));
super.setupViews();
mActionsView = findViewById(R.id.overview_actions_view);
RecentsView<?,?> overviewPanel = getOverviewPanel();
SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
mSplitSelectStateController =
new SplitSelectStateController(this, getStateManager(),
new SplitSelectStateController(this, mHandler, getStateManager(),
getDepthController(), getStatsLogManager(),
systemUiProxy, RecentsModel.INSTANCE.get(this),
() -> onStateBack());
if (DesktopModeStatus.canEnterDesktopMode(this) && LawnchairApp.isRecentsEnabled()) {
RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(asContext());
// TODO(b/337863494): Explore use of the same OverviewComponentObserver across launcher
OverviewComponentObserver overviewComponentObserver = new OverviewComponentObserver(
asContext(), deviceState);
if (enableDesktopWindowingMode() && LawnchairApp.isRecentsEnabled()) {
mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
getStateManager(), systemUiProxy, LawnchairApp.getInstance().getIApplicationThread(),
getDepthController());
}
overviewPanel.init(mActionsView, mSplitSelectStateController,
mDesktopRecentsTransitionController);
mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(
this, mSplitSelectStateController);
mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(this,
mSplitSelectStateController, overviewComponentObserver, deviceState);
mSplitToWorkspaceController = new SplitToWorkspaceController(this,
mSplitSelectStateController);
mActionsView.updateDimension(getDeviceProfile(), overviewPanel.getLastComputedTaskSize());
@@ -332,8 +291,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
mDepthController = new DepthController(this);
if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(this)) {
mSplitSelectStateController.initSplitFromDesktopController(this);
if (enableDesktopWindowingMode()) {
mDesktopVisibilityController = new DesktopVisibilityController(this);
mDesktopVisibilityController.registerSystemUiListener();
mSplitSelectStateController.initSplitFromDesktopController(this,
overviewComponentObserver);
}
mHotseatPredictionController = new HotseatPredictionController(this);
@@ -347,7 +309,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
@Override
public void logAppLaunch(StatsLogManager statsLogManager, ItemInfo info,
InstanceId instanceId) {
InstanceId instanceId) {
// If the app launch is from any of the surfaces in AllApps then add the InstanceId from
// LiveSearchManager to recreate the AllApps session on the server side.
if (mAllAppsSessionLogId != null && ALL_APPS.equals(
@@ -379,7 +341,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
@Override
protected void completeAddShortcut(Intent data, int container, int screenId, int cellX,
int cellY, PendingRequestArgs args) {
int cellY, PendingRequestArgs args) {
if (container == CONTAINER_HOTSEAT) {
mHotseatPredictionController.onDeferredDrop(cellX, cellY);
}
@@ -471,7 +433,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
protected void onItemClicked(View view) {
if (!mSplitToWorkspaceController.handleSecondAppSelectionForSplit(view)) {
super.getItemOnClickListener().onClick(view);
QuickstepLauncher.super.getItemOnClickListener().onClick(view);
}
}
@@ -485,7 +447,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
// Order matters as it affects order of appearance in popup container
List<SystemShortcut.Factory> shortcuts = new ArrayList(Arrays.asList(
APP_INFO, WellbeingModel.SHORTCUT_FACTORY, mHotseatPredictionController));
shortcuts.addAll(getSplitShortcuts());
shortcuts.add(WIDGETS);
shortcuts.add(INSTALL);
@@ -498,9 +459,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
if (Flags.enablePrivateSpace()) {
shortcuts.add(UNINSTALL_APP);
}
if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
shortcuts.add(BUBBLE_SHORTCUT);
}
return shortcuts.stream();
}
@@ -528,10 +486,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
boolean started = ((getActivityFlags() & ACTIVITY_STATE_STARTED)) != 0;
if (started) {
DeviceProfile profile = getDeviceProfile();
boolean willUserBeActive =
(getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0;
boolean visible = (state == NORMAL || state == OVERVIEW)
&& isUserActive()
&& !profile.isVerticalBarLayout()
&& !mIsOverlayVisible;
&& (willUserBeActive || isUserActive())
&& !profile.isVerticalBarLayout();
SystemUiProxy.INSTANCE.get(this)
.setLauncherKeepClearAreaHeight(visible, profile.hotseatBarSizePx);
}
@@ -540,12 +499,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
}
}
@Override
public void onOverlayVisibilityChanged(boolean visible) {
super.onOverlayVisibilityChanged(visible);
mIsOverlayVisible = visible;
}
@Override
public void bindExtraContainerItems(FixedContainerItems item) {
if (item.containerId == Favorites.CONTAINER_PREDICTION) {
@@ -557,7 +510,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
} else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
mHotseatPredictionController.setPredictedItems(item);
} else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) {
getWidgetPickerDataProvider().setWidgetRecommendations(item.items);
getPopupDataProvider().setRecommendedWidgets(item.items);
}
}
@@ -580,25 +533,23 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
mUnfoldTransitionProgressProvider.destroy();
}
OverviewComponentObserver.INSTANCE.get(this)
.removeOverviewChangeListener(mOverviewChangeListener);
mTISBindHelper.onDestroy();
if (mLauncherUnfoldAnimationController != null) {
mLauncherUnfoldAnimationController.onDestroy();
}
if (mDesktopVisibilityController != null) {
mDesktopVisibilityController.unregisterSystemUiListener();
}
if (mSplitSelectStateController != null) {
mSplitSelectStateController.onDestroy();
}
RecentsView recentsView = getOverviewPanel();
if (recentsView != null) {
recentsView.destroy();
}
super.onDestroy();
mHotseatPredictionController.destroy();
mSplitWithKeyboardShortcutController.onDestroy();
if (mViewCapture != null) mViewCapture.close();
if (Utilities.ATLEAST_U) {
removeBackAnimationCallback(mSplitSelectStateController.getSplitBackHandler());
@@ -632,16 +583,9 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
}
case QUICK_SWITCH_STATE_ORDINAL: {
RecentsView rv = getOverviewPanel();
TaskView currentPageTask = rv.getCurrentPageTaskView();
TaskView fallbackTask = rv.getFirstTaskView();
if (currentPageTask != null || fallbackTask != null) {
TaskView taskToLaunch = currentPageTask;
if (currentPageTask == null) {
taskToLaunch = fallbackTask;
ActiveGestureProtoLogProxy.logQuickSwitchFromHomeFallback(
rv.getCurrentPage());
}
taskToLaunch.launchWithoutAnimation(success -> {
TaskView tasktolaunch = rv.getCurrentPageTaskView();
if (tasktolaunch != null) {
tasktolaunch.launchTask(success -> {
if (!success) {
getStateManager().goToState(OVERVIEW);
} else {
@@ -650,7 +594,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
return Unit.INSTANCE;
});
} else {
ActiveGestureProtoLogProxy.logQuickSwitchFromHomeFailed(rv.getCurrentPage());
getStateManager().goToState(NORMAL);
}
break;
@@ -697,12 +640,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
list.add(new StatusBarTouchController(this));
}
if (enableExpressiveDismissTaskMotion()) {
list.add(new TaskViewLaunchTouchController<>(this, mTaskViewRecentsTouchContext));
list.add(new TaskViewDismissTouchController<>(this, mTaskViewRecentsTouchContext));
} else {
list.add(new TaskViewTouchControllerDeprecated<>(this, mTaskViewRecentsTouchContext));
}
list.add(new LauncherTaskViewController(this));
return list.toArray(new TouchController[list.size()]);
}
@@ -711,8 +649,28 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
return new QuickstepAtomicAnimationFactory(this);
}
@Override
protected LauncherWidgetHolder createAppWidgetHolder() {
final QuickstepHolderFactory factory =
(QuickstepHolderFactory) LauncherWidgetHolder.HolderFactory.newFactory(this);
return factory.newInstance(this,
appWidgetId -> getWorkspace().removeWidget(appWidgetId),
new QuickstepInteractionHandler(this));
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// Back dispatcher is registered in {@link BaseActivity#onCreate}. For predictive back to
// work, we must opt-in BEFORE registering back dispatcher. So we need to call
// setEnableOnBackInvokedCallback() before super.onCreate()
if (Utilities.ATLEAST_U && enablePredictiveBackGesture()) {
try {
getApplicationInfo().setEnableOnBackInvokedCallback(true);
} catch (NoSuchMethodError e) {
// Ignore
}
}
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mPendingSplitSelectInfo = ObjectWrapper.unwrap(
@@ -720,29 +678,21 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
}
addMultiWindowModeChangedListener(mDepthController);
initUnfoldTransitionProgressProvider();
mViewCapture = ViewCaptureFactory.getInstance(this).startCapture(getWindow());
if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) {
mViewCapture = ViewCaptureFactory.getInstance(this).startCapture(getWindow());
}
// getWindow().addPrivateFlags(PRIVATE_FLAG_OPTIMIZE_MEASURE);
QuickstepOnboardingPrefs.setup(this);
// View.setTraceLayoutSteps(TRACE_LAYOUTS);
// View.setTracedRequestLayoutClassClass(TRACE_RELAYOUT_CLASS);
OverviewComponentObserver.INSTANCE.get(this)
.addOverviewChangeListener(mOverviewChangeListener);
}
@Override
protected boolean initDeviceProfile(InvariantDeviceProfile idp) {
final boolean ret = super.initDeviceProfile(idp);
try {
mDeviceProfile.isPredictiveBackSwipe =
getApplicationInfo().isOnBackInvokedCallbackEnabled();
} catch (Throwable t) {
mDeviceProfile.isPredictiveBackSwipe = false;
}
if (ATLEAST_S_V2) {
if (ret) {
SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx);
}
}
// mDeviceProfile.isPredictiveBackSwipe =
// getApplicationInfo().isOnBackInvokedCallbackEnabled();
mDeviceProfile.isPredictiveBackSwipe = false;
return ret;
}
@@ -752,7 +702,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
// Check if there is already an instance of this app running, if so, initiate the split
// using that.
mSplitSelectStateController.findLastActiveTasksAndRunCallback(
Collections.singletonList(splitSelectSource.getItemInfo().getComponentKey()),
Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
false /* findExactPairMatch */,
foundTasks -> {
@Nullable Task foundTask = foundTasks[0];
@@ -760,7 +710,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
splitSelectSource.alreadyRunningTaskId = taskWasFound
? foundTask.key.id
: INVALID_TASK_ID;
startSplitToHome(splitSelectSource);
if (enableSplitContextually()) {
startSplitToHome(splitSelectSource);
} else {
recentsView.initiateSplitSelect(splitSelectSource);
}
}
);
}
@@ -775,7 +729,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
Rect tempRect = new Rect();
mSplitSelectStateController.setInitialTaskSelect(source.intent,
source.position.stagePosition, source.getItemInfo(), source.splitEvent,
source.position.stagePosition, source.itemInfo, source.splitEvent,
source.alreadyRunningTaskId);
RecentsView recentsView = getOverviewPanel();
@@ -793,8 +747,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
floatingTaskView.setOnClickListener(view ->
mSplitSelectStateController.getSplitAnimationController().
playAnimPlaceholderToFullscreen(this, view, Optional.empty()));
floatingTaskView.setContentDescription(source.getItemInfo().contentDescription);
mSplitSelectStateController.setFirstFloatingTaskView(floatingTaskView);
anim.addListener(new AnimatorListenerAdapter() {
@Override
@@ -836,10 +788,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
if (mLauncherUnfoldAnimationController != null) {
mLauncherUnfoldAnimationController.onResume();
}
if (mTaskbarUIController != null && FeatureFlags.enableHomeTransitionListener()) {
mTaskbarUIController.onLauncherResume();
}
}
@Override
@@ -850,25 +798,15 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
super.onPause();
// If Launcher pauses before both split apps are selected, exit split screen.
if (!mSplitSelectStateController.isBothSplitAppsConfirmed() &&
!mSplitSelectStateController.isLaunchingFirstAppFullscreen()) {
mSplitSelectStateController
.logExitReason(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
mSplitSelectStateController.getSplitAnimationController()
.playPlaceholderDismissAnim(this, LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
}
if (mTaskbarUIController != null && FeatureFlags.enableHomeTransitionListener()) {
mTaskbarUIController.onLauncherPause();
}
}
@Override
protected void onStop() {
super.onStop();
if (mTaskbarUIController != null && FeatureFlags.enableHomeTransitionListener()) {
mTaskbarUIController.onLauncherStop();
if (enableSplitContextually()) {
// If Launcher pauses before both split apps are selected, exit split screen.
if (!mSplitSelectStateController.isBothSplitAppsConfirmed() &&
!mSplitSelectStateController.isLaunchingFirstAppFullscreen()) {
mSplitSelectStateController
.logExitReason(LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
mSplitSelectStateController.getSplitAnimationController()
.playPlaceholderDismissAnim(this, LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED);
}
}
}
@@ -943,67 +881,71 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
// event won't go through ViewRootImpl#InputStage#onProcess.
// So when receive back key, try to do the same check thing in
// ViewRootImpl#NativePreImeInputStage#onProcess
if (event.getKeyCode() != KeyEvent.KEYCODE_BACK
if (!Utilities.ATLEAST_U || !enablePredictiveBackGesture()
|| event.getKeyCode() != KeyEvent.KEYCODE_BACK
|| event.getAction() != KeyEvent.ACTION_UP || event.isCanceled()) {
return false;
}
// Lawnchair-TODO-Merge: LC disabled this, 16r2 enabled it.
// getOnBackAnimationCallback().onBackInvoked();
return true;
}
@Override
protected void registerBackDispatcher() {
if (Utilities.ATLEAST_U) {
if (!enablePredictiveBackGesture()) {
super.registerBackDispatcher();
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
new FlingOnBackAnimationCallback() {
@Nullable OnBackAnimationCallback mActiveOnBackAnimationCallback;
new OnBackAnimationCallback() {
@Nullable
OnBackPressedHandler mActiveOnBackPressedHandler;
@Override
public void onBackStartedCompat(@NonNull BackEvent backEvent) {
if (mActiveOnBackAnimationCallback != null) {
mActiveOnBackAnimationCallback.onBackCancelled();
public void onBackStarted(@NonNull BackEvent backEvent) {
if (mActiveOnBackPressedHandler != null) {
mActiveOnBackPressedHandler.onBackCancelled();
}
mActiveOnBackAnimationCallback = getOnBackAnimationCallback();
mActiveOnBackAnimationCallback.onBackStarted(backEvent);
mActiveOnBackPressedHandler = getOnBackPressedHandler();
mActiveOnBackPressedHandler.onBackStarted();
}
@Override
public void onBackInvokedCompat() {
// Recreate mActiveOnBackAnimationCallback if necessary to avoid NPE
public void onBackInvoked() {
// Recreate mActiveOnBackPressedHandler if necessary to avoid NPE
// because:
// 1. b/260636433: In 3-button-navigation mode, onBackStarted() is not
// called on ACTION_DOWN before onBackInvoked() is called in ACTION_UP.
// 2. Launcher#onBackPressed() will call onBackInvoked() without calling
// onBackInvoked() beforehand.
if (mActiveOnBackAnimationCallback == null) {
mActiveOnBackAnimationCallback = getOnBackAnimationCallback();
if (mActiveOnBackPressedHandler == null) {
mActiveOnBackPressedHandler = getOnBackPressedHandler();
}
mActiveOnBackAnimationCallback.onBackInvoked();
mActiveOnBackAnimationCallback = null;
mActiveOnBackPressedHandler.onBackInvoked();
mActiveOnBackPressedHandler = null;
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onBackInvoked");
}
@Override
public void onBackProgressedCompat(@NonNull BackEvent backEvent) {
public void onBackProgressed(@NonNull BackEvent backEvent) {
if (!FeatureFlags.IS_STUDIO_BUILD
&& mActiveOnBackAnimationCallback == null) {
&& mActiveOnBackPressedHandler == null) {
return;
}
mActiveOnBackAnimationCallback.onBackProgressed(backEvent);
mActiveOnBackPressedHandler.onBackProgressed(backEvent.getProgress());
}
@Override
public void onBackCancelledCompat() {
public void onBackCancelled() {
if (!FeatureFlags.IS_STUDIO_BUILD
&& mActiveOnBackAnimationCallback == null) {
&& mActiveOnBackPressedHandler == null) {
return;
}
mActiveOnBackAnimationCallback.onBackCancelled();
mActiveOnBackAnimationCallback = null;
mActiveOnBackPressedHandler.onBackCancelled();
mActiveOnBackPressedHandler = null;
}
});
}
@@ -1021,7 +963,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
@Override
public void startIntentSenderForResult(IntentSender intent, int requestCode,
Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) {
Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) {
if (requestCode != -1) {
mPendingActivityRequestCode = requestCode;
StartActivityParams params = new StartActivityParams(this, requestCode);
@@ -1053,16 +995,13 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
@Override
public void setResumed() {
DesktopVisibilityController desktopVisibilityController =
DesktopVisibilityController.INSTANCE.get(this);
if (ATLEAST_BAKLAVA) {
if (!ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
&& desktopVisibilityController.isInDesktopModeAndNotInOverview(getDisplayId())
&& !desktopVisibilityController.isRecentsGestureInProgress()) {
// Return early to skip setting activity to appear as resumed
// TODO: b/333533253 - Remove after flag rollout
return;
}
if (!enableDesktopWindowingWallpaperActivity()
&& mDesktopVisibilityController != null
&& mDesktopVisibilityController.areDesktopTasksVisible()
&& !mDesktopVisibilityController.isRecentsGestureInProgress()) {
// Return early to skip setting activity to appear as resumed
// TODO: b/333533253 - Remove after flag rollout
return;
}
super.setResumed();
}
@@ -1084,13 +1023,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
}
}
private void onOverviewTargetChanged(boolean isHomeAndOverviewSame) {
QuickstepTransitionManager transitionManager = getAppTransitionManager();
if (transitionManager != null) {
transitionManager.onOverviewTargetChange();
}
}
private void onTISConnected(TISBinder binder) {
TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
if (taskbarManager != null) {
@@ -1121,6 +1053,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
/** Receives animation progress from sysui process. */
private void initRemotelyCalculatedUnfoldAnimation(UnfoldTransitionConfig config) {
RemoteUnfoldSharedComponent unfoldComponent =
UnfoldTransitionFactory.createRemoteUnfoldSharedComponent(
/* context= */ this,
@@ -1148,7 +1081,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
}
private void initUnfoldAnimationController(UnfoldTransitionProgressProvider progressProvider,
@UnfoldMain RotationChangeProvider rotationChangeProvider) {
@UnfoldMain RotationChangeProvider rotationChangeProvider) {
mLauncherUnfoldAnimationController = new LauncherUnfoldAnimationController(
/* launcher= */ this,
getWindowManager(),
@@ -1157,32 +1090,14 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
);
}
@Override
public void setTaskbarUIController(@Nullable TaskbarUIController taskbarUIController) {
mTaskbarUIController = (LauncherTaskbarUIController) taskbarUIController;
public void setTaskbarUIController(LauncherTaskbarUIController taskbarUIController) {
mTaskbarUIController = taskbarUIController;
}
@Override
public @Nullable LauncherTaskbarUIController getTaskbarUIController() {
return mTaskbarUIController;
}
/** Provides the translation X for the hotseat item. */
public int getHotseatItemTranslationX(ItemInfo itemInfo) {
int translationX = 0;
if (isBubbleBarEnabled() && mBubbleBarLocation != null) {
boolean isBubblesOnLeft = mBubbleBarLocation.isOnLeft(isRtl(getResources()));
translationX += mDeviceProfile
.getHotseatTranslationXForNavBar(this, isBubblesOnLeft);
}
if (isBubbleBarEnabled()
&& mDeviceProfile.shouldAdjustHotseatForBubbleBar(asContext(), hasBubbles())) {
translationX += (int) mDeviceProfile
.getHotseatAdjustedTranslation(asContext(), itemInfo.cellX);
}
return translationX;
}
public SplitToWorkspaceController getSplitToWorkspaceController() {
return mSplitToWorkspaceController;
}
@@ -1223,6 +1138,11 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
return mDepthController;
}
@Nullable
public DesktopVisibilityController getDesktopVisibilityController() {
return mDesktopVisibilityController;
}
@Nullable
public UnfoldTransitionProgressProvider getUnfoldTransitionProgressProvider() {
return mUnfoldTransitionProgressProvider;
@@ -1253,23 +1173,19 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
}
}
@NonNull
@Override
public ActivityOptionsWrapper getActivityLaunchOptions(View v, @Nullable ItemInfo item) {
ActivityOptionsWrapper activityOptions = mAppTransitionManager.getActivityLaunchOptions(
v, item != null ? item : (ItemInfo) v.getTag());
ActivityOptionsWrapper activityOptions = mAppTransitionManager.getActivityLaunchOptions(v);
if (mLastTouchUpTime > 0) {
activityOptions.options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_LAUNCHER,
mLastTouchUpTime);
}
if (ATLEAST_T) {
if (item != null && (item.animationType == DEFAULT_NO_ICON
if (item != null && (item.animationType == DEFAULT_NO_ICON
|| item.animationType == VIEW_BACKGROUND)) {
activityOptions.options.setSplashScreenStyle(
activityOptions.options.setSplashScreenStyle(
SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR);
} else {
activityOptions.options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
}
} else {
activityOptions.options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
}
activityOptions.options.setLaunchDisplayId(
(v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId()
@@ -1282,11 +1198,9 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
public ActivityOptionsWrapper makeDefaultActivityOptions(int splashScreenStyle) {
RunnableList callbacks = new RunnableList();
ActivityOptions options = ActivityOptions.makeCustomAnimation(this, 0, 0);
if (ATLEAST_T) {
options.setSplashScreenStyle(splashScreenStyle);
}
options.setSplashScreenStyle(splashScreenStyle);
Utilities.allowBGLaunch(options);
IRemoteCallback endCallback = completeRunnableListCallback(callbacks, this);
IRemoteCallback endCallback = completeRunnableListCallback(callbacks);
options.setOnAnimationAbortListener(endCallback);
options.setOnAnimationFinishedListener(endCallback);
return new ActivityOptionsWrapper(options, callbacks);
@@ -1374,14 +1288,18 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
mTISBindHelper.setPredictiveBackToHomeInProgress(isInProgress);
}
public boolean getPredictiveBackToHomeInProgress() {
return mIsPredictiveBackToHomeInProgress;
@Override
public boolean areDesktopTasksVisible() {
if (mDesktopVisibilityController != null) {
return mDesktopVisibilityController.areDesktopTasksVisible();
}
return false;
}
@Override
public boolean areDesktopTasksVisible() {
return DesktopVisibilityController.INSTANCE.get(this)
.isInDesktopModeAndNotInOverview(getDisplayId());
protected void onDeviceProfileInitiated() {
super.onDeviceProfileInitiated();
SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx);
}
@Override
@@ -1390,23 +1308,29 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx);
TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
if (taskbarManager != null) {
taskbarManager.debugPrimaryTaskbar("QuickstepLauncher#onDeviceProfileChanged",
true);
taskbarManager.debugWhyTaskbarNotDestroyed("QuickstepLauncher#onDeviceProfileChanged");
}
}
/**
* Launches the given {@link SplitTask} in splitscreen.
* Launches the given {@link GroupTask} in splitscreen.
*/
public void launchSplitTasks(
@NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) {
mSplitSelectStateController.launchExistingSplitPair(null /* launchingTaskView */,
splitTask.getTopLeftTask().key.id,
splitTask.getBottomRightTask().key.id,
@NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) {
// Top/left and bottom/right tasks respectively.
Task task1 = groupTask.task1;
// task2 should never be null when calling this method. Allow a crash to catch invalid calls
Task task2 = groupTask.task2;
mSplitSelectStateController.launchExistingSplitPair(
null /* launchingTaskView */,
task1.key.id,
task2.key.id,
SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
/* callback= */ success -> mSplitSelectStateController.resetState(),
/* freezeTaskList= */ false,
splitTask.getSplitBounds().snapPosition,
groupTask.mSplitBounds == null
? SNAP_TO_50_50
: groupTask.mSplitBounds.snapPosition,
remoteTransition);
}
@@ -1414,14 +1338,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
* Launches two apps as an app pair.
*/
public void launchAppPair(AppPairIcon appPairIcon) {
// Potentially show the Taskbar education once the app pair launch finishes
mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon,
CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE,
(success) -> {
if (success && mTaskbarUIController != null) {
mTaskbarUIController.showEduOnAppLaunch();
}
});
CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE);
}
public boolean canStartHomeSafely() {
@@ -1439,69 +1357,41 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
return (mTaskbarUIController != null && mTaskbarUIController.hasBubbles());
}
@NonNull
public TISBindHelper getTISBindHelper() {
return mTISBindHelper;
}
@Override
public boolean handleIncorrectSplitTargetSelection() {
if (!mSplitSelectStateController.isSplitSelectActive()) {
if (!enableSplitContextually() || !mSplitSelectStateController.isSplitSelectActive()) {
return false;
}
mSplitSelectStateController.getSplitInstructionsView().goBoing();
return true;
}
@Override
public void showShortcutBubble(ShortcutInfo info) {
if (info == null) return;
SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info);
}
private static final class LauncherTaskViewController extends
TaskViewTouchController<QuickstepLauncher> {
@Override
public void showAppBubble(Intent intent, UserHandle user) {
if (intent == null || intent.getPackage() == null) return;
SystemUiProxy.INSTANCE.get(this).showAppBubble(intent, user);
}
/** Sets the location of the bubble bar */
public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
mBubbleBarLocation = bubbleBarLocation;
}
/**
* Similar to {@link #getFirstHomeElementForAppClose} but also matches all apps if its visible
*/
@Nullable
public View getFirstVisibleElementForAppClose(
@Nullable StableViewInfo svi, String packageName, UserHandle user) {
if (isInState(LauncherState.ALL_APPS)) {
AllAppsRecyclerView activeRecyclerView = getAppsView().getActiveRecyclerView();
View v = null;
if (svi != null) {
// Preferred item match
v = activeRecyclerView.findViewByPredicate(view ->
view.isAggregatedVisible()
&& view.getTag() instanceof ItemInfo info && svi.matches(info));
}
if (v == null) {
// Package user match
v = activeRecyclerView.findViewByPredicate(view ->
view.isAggregatedVisible() && view.getTag() instanceof ItemInfo info
&& info.itemType == ITEM_TYPE_APPLICATION
&& info.user.equals(user)
&& TextUtils.equals(info.getTargetPackage(), packageName));
}
if (v != null && activeRecyclerView.computeVerticalScrollOffset() > 0) {
RectF locationBounds = new RectF();
FloatingIconView.getLocationBoundsForView(this, v, false, locationBounds,
new Rect());
if (locationBounds.top < getAppsView().getHeaderBottom()) {
// Icon is covered by scrim, return null to play fallback animation.
return null;
}
}
return v;
LauncherTaskViewController(QuickstepLauncher activity) {
super(activity);
}
return getFirstHomeElementForAppClose(svi, packageName, user);
@Override
protected boolean isRecentsInteractive() {
return mContainer.isInState(OVERVIEW) || mContainer.isInState(OVERVIEW_MODAL_TASK);
}
@Override
protected boolean isRecentsModal() {
return mContainer.isInState(OVERVIEW_MODAL_TASK);
}
@Override
protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
mContainer.getStateManager().setCurrentUserControlledAnimation(animController);
}
}
@Override
@@ -1531,18 +1421,6 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
switch (name) {
case "TextClock", "android.widget.TextClock" -> {
TextClock tc = new TextClock(context, attrs);
tc.setClockEventDelegate(AsyncClockEventDelegate.INSTANCE.get(this));
return tc;
}
case "AnalogClock", "android.widget.AnalogClock" -> {
AnalogClock ac = new AnalogClock(context, attrs);
ac.setClockEventDelegate(AsyncClockEventDelegate.INSTANCE.get(this));
return ac;
}
}
return super.onCreateView(parent, name, context, attrs);
}
@@ -1550,17 +1428,4 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
public boolean isRecentsViewVisible() {
return getStateManager().getState().isRecentsViewVisible;
}
public boolean isCanShowAllAppsEducationView() {
return mCanShowAllAppsEducationView;
}
public void setCanShowAllAppsEducationView(boolean canShowAllAppsEducationView) {
mCanShowAllAppsEducationView = canShowAllAppsEducationView;
}
@Override
public void returnToHomescreen() {
getStateManager().goToState(LauncherState.NORMAL);
}
}
@@ -16,40 +16,43 @@
package com.android.launcher3.uioverrides;
import static com.android.launcher3.BuildConfigs.WIDGETS_ENABLED;
import static com.android.launcher3.uioverrides.QuickstepAppWidgetHostProvider.getStaticQuickstepHost;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.widget.ListenableAppWidgetHost.getWidgetHolderExecutor;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.util.Log;
import android.util.SparseArray;
import android.widget.RemoteViews;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.LauncherWidgetHolder;
import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
import dagger.assisted.AssistedInject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.BiConsumer;
import java.util.function.IntConsumer;
/**
* {@link LauncherWidgetHolder} that puts the app widget host in the background
*/
public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
public class QuickstepWidgetHolder extends LauncherWidgetHolder {
private static final String TAG = "QuickstepWidgetHolder";
private static final UpdateKey<AppWidgetProviderInfo> KEY_PROVIDER_UPDATE =
AppWidgetHostView::onUpdateProviderInfo;
@@ -58,17 +61,51 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
private static final UpdateKey<Integer> KEY_VIEW_DATA_CHANGED =
AppWidgetHostView::onViewDataChanged;
private static final List<QuickstepWidgetHolder> sHolders = new ArrayList<>();
private static final SparseArray<QuickstepWidgetHolderListener> sListeners =
new SparseArray<>();
private static AppWidgetHost sWidgetHost = null;
private final UpdateHandler mUpdateHandler = this::onWidgetUpdate;
private final @Nullable RemoteViews.InteractionHandler mInteractionHandler;
private final @NonNull IntConsumer mAppWidgetRemovedCallback;
// Map to all pending updated keyed with appWidgetId;
private final SparseArray<PendingUpdate> mPendingUpdateMap = new SparseArray<>();
@AssistedInject
public QuickstepWidgetHolder(@Assisted("UI_CONTEXT") @NonNull Context context) {
super(context, getStaticQuickstepHost());
public QuickstepWidgetHolder(@NonNull Context context,
@Nullable IntConsumer appWidgetRemovedCallback,
@Nullable RemoteViews.InteractionHandler interactionHandler) {
super(context, appWidgetRemovedCallback);
mAppWidgetRemovedCallback = appWidgetRemovedCallback != null ? appWidgetRemovedCallback
: i -> {};
mInteractionHandler = interactionHandler;
MAIN_EXECUTOR.execute(() -> sHolders.add(this));
}
@Override
@NonNull
protected AppWidgetHost createHost(@NonNull Context context,
@Nullable IntConsumer appWidgetRemovedCallback) {
if (sWidgetHost == null) {
sWidgetHost = new QuickstepAppWidgetHost(context.getApplicationContext(),
i -> MAIN_EXECUTOR.execute(() ->
sHolders.forEach(h -> h.mAppWidgetRemovedCallback.accept(i))),
() -> MAIN_EXECUTOR.execute(() ->
sHolders.forEach(h ->
// Listeners might remove themselves from the list during the
// iteration. Creating a copy of the list to avoid exceptions
// for concurrent modification.
new ArrayList<>(h.mProviderChangedListeners).forEach(
ProviderChangedListener::notifyWidgetProvidersChanged))),
UI_HELPER_EXECUTOR.getLooper());
if (WIDGETS_ENABLED) {
sWidgetHost.startListening();
}
}
return sWidgetHost;
}
@Override
@@ -132,6 +169,21 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
sListeners.remove(appWidgetId);
}
/**
* Called when the launcher is destroyed
*/
@Override
public void destroy() {
try {
MAIN_EXECUTOR.submit(() -> {
clearViews();
sHolders.remove(this);
}).get();
} catch (Exception e) {
Log.e(TAG, "Failed to remove self from holder list", e);
}
}
@Override
protected boolean shouldListen(int flags) {
return (flags & (FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED))
@@ -147,10 +199,12 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
return;
}
getWidgetHolderExecutor().execute(() -> {
mWidgetHost.setAppWidgetHidden();
setListeningFlag(false);
});
try {
sWidgetHost.setAppWidgetHidden();
} catch (Throwable t) {
// Ignore
}
setListeningFlag(false);
}
@Override
@@ -166,7 +220,7 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
};
QuickstepWidgetHolderListener holderListener = getHolderListener(appWidgetId);
holderListener.addHolder(handler);
return () -> holderListener.removeHolder(handler);
return () -> holderListener.mListeningHolders.remove(handler);
}
/**
@@ -188,6 +242,7 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
protected LauncherAppWidgetHostView createViewInternal(
int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
LauncherAppWidgetHostView widgetView = new LauncherAppWidgetHostView(mContext);
widgetView.setInteractionHandler(mInteractionHandler);
widgetView.setAppWidget(appWidgetId, appWidget);
widgetView.updateAppWidget(getHolderListener(appWidgetId).addHolder(mUpdateHandler));
return widgetView;
@@ -197,7 +252,7 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
QuickstepWidgetHolderListener listener = sListeners.get(appWidgetId);
if (listener == null) {
listener = new QuickstepWidgetHolderListener(appWidgetId);
getStaticQuickstepHost().setListener(appWidgetId, listener);
sWidgetHost.setListener(appWidgetId, listener);
sListeners.put(appWidgetId, listener);
}
return listener;
@@ -210,7 +265,7 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
public void clearViews() {
mViews.clear();
for (int i = sListeners.size() - 1; i >= 0; i--) {
sListeners.valueAt(i).removeHolder(mUpdateHandler);
sListeners.valueAt(i).mListeningHolders.remove(mUpdateHandler);
}
}
@@ -237,15 +292,13 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
mWidgetId = widgetId;
}
@UiThread
@Nullable
public RemoteViews addHolder(@NonNull UpdateHandler holder) {
MAIN_EXECUTOR.execute(() -> mListeningHolders.add(holder));
mListeningHolders.add(holder);
return mRemoteViews;
}
public void removeHolder(@NonNull UpdateHandler holder) {
MAIN_EXECUTOR.execute(() -> mListeningHolders.remove(holder));
}
@Override
@AnyThread
public void onUpdateProviderInfo(@Nullable AppWidgetProviderInfo info) {
@@ -272,13 +325,44 @@ public final class QuickstepWidgetHolder extends LauncherWidgetHolder {
}
}
/**
* {@code HolderFactory} subclass that takes an interaction handler as one of the parameters
* when creating a new instance.
*/
public static class QuickstepHolderFactory extends HolderFactory {
/** A factory that generates new instances of {@code LauncherWidgetHolder} */
@AssistedFactory
public interface QuickstepWidgetHolderFactory extends WidgetHolderFactory {
@SuppressWarnings("unused")
public QuickstepHolderFactory(Context context) { }
@Override
QuickstepWidgetHolder newInstance(@Assisted("UI_CONTEXT") @NonNull Context context);
public LauncherWidgetHolder newInstance(@NonNull Context context,
@Nullable IntConsumer appWidgetRemovedCallback) {
return newInstance(context, appWidgetRemovedCallback, null);
}
/**
* @param context The context of the caller
* @param appWidgetRemovedCallback The callback that is called when widgets are removed
* @param interactionHandler The interaction handler when the widgets are clicked
* @return A new {@link LauncherWidgetHolder} instance
*/
public LauncherWidgetHolder newInstance(@NonNull Context context,
@Nullable IntConsumer appWidgetRemovedCallback,
@Nullable RemoteViews.InteractionHandler interactionHandler) {
if (!FeatureFlags.ENABLE_WIDGET_HOST_IN_BACKGROUND.get()) {
return new LauncherWidgetHolder(context, appWidgetRemovedCallback) {
@Override
protected AppWidgetHost createHost(Context context,
@Nullable IntConsumer appWidgetRemovedCallback) {
AppWidgetHost host = super.createHost(context, appWidgetRemovedCallback);
host.setInteractionHandler(interactionHandler);
return host;
}
};
}
return new QuickstepWidgetHolder(context, appWidgetRemovedCallback, interactionHandler);
}
}
private interface UpdateKey<T> extends BiConsumer<AppWidgetHostView, T> { }

Some files were not shown because too many files have changed in this diff Show More