From 77b3419ad55a4f9070cbe7d9dcb089dbc2b96114 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Fri, 19 Apr 2019 01:46:51 -0700 Subject: Adding support for showing predicted apps as a floating row in all-apps and overview Bug: 130053407 Change-Id: Idb93a0ba6cfea8406f75ab86d9e0acde2fc04b3a --- .../drawable/arrow_toast_rounded_background.xml | 19 + .../res/layout/arrow_toast.xml | 62 ++++ .../res/layout/floating_header_content.xml | 14 + .../res/layout/prediction_load_progress.xml | 11 + .../recents_ui_overrides/res/values/colors.xml | 5 + .../recents_ui_overrides/res/values/dimens.xml | 9 + .../recents_ui_overrides/res/values/override.xml | 2 + .../android/launcher3/LauncherInitListenerEx.java | 34 ++ .../launcher3/appprediction/AllAppsTipView.java | 207 +++++++++++ .../launcher3/appprediction/AppsDividerView.java | 308 +++++++++++++++ .../appprediction/ComponentKeyMapper.java | 69 ++++ .../launcher3/appprediction/DynamicItemCache.java | 242 ++++++++++++ .../appprediction/InstantAppItemInfo.java | 50 +++ .../appprediction/PredictionAppTracker.java | 214 +++++++++++ .../launcher3/appprediction/PredictionRowView.java | 411 +++++++++++++++++++++ .../appprediction/PredictionUiStateManager.java | 330 +++++++++++++++++ .../LauncherActivityControllerHelper.java | 4 +- .../com/android/quickstep/TaskOverlayFactory.java | 2 + .../quickstep/views/LauncherRecentsView.java | 13 +- .../com/android/quickstep/views/RecentsView.java | 4 +- quickstep/res/values/strings.xml | 9 + .../com/android/quickstep/OverviewCallbacks.java | 2 - .../logging/UserEventDispatcherExtension.java | 2 + .../android/quickstep/AppPredictionsUITests.java | 168 +++++++++ 24 files changed, 2183 insertions(+), 8 deletions(-) create mode 100644 quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml create mode 100644 quickstep/recents_ui_overrides/res/layout/arrow_toast.xml create mode 100644 quickstep/recents_ui_overrides/res/layout/floating_header_content.xml create mode 100644 quickstep/recents_ui_overrides/res/layout/prediction_load_progress.xml create mode 100644 quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java create mode 100644 quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java create mode 100644 quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java create mode 100644 quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java create mode 100644 quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java create mode 100644 quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java create mode 100644 quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java create mode 100644 quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java create mode 100644 quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java create mode 100644 quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java (limited to 'quickstep') diff --git a/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml b/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml new file mode 100644 index 000000000..52cc6fcb6 --- /dev/null +++ b/quickstep/recents_ui_overrides/res/drawable/arrow_toast_rounded_background.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml b/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml new file mode 100644 index 000000000..b0f2b4bf8 --- /dev/null +++ b/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + diff --git a/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml b/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml new file mode 100644 index 000000000..b21c34b09 --- /dev/null +++ b/quickstep/recents_ui_overrides/res/layout/floating_header_content.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/quickstep/recents_ui_overrides/res/layout/prediction_load_progress.xml b/quickstep/recents_ui_overrides/res/layout/prediction_load_progress.xml new file mode 100644 index 000000000..20c400441 --- /dev/null +++ b/quickstep/recents_ui_overrides/res/layout/prediction_load_progress.xml @@ -0,0 +1,11 @@ + + diff --git a/quickstep/recents_ui_overrides/res/values/colors.xml b/quickstep/recents_ui_overrides/res/values/colors.xml index 1e8d0cc30..7426e3039 100644 --- a/quickstep/recents_ui_overrides/res/values/colors.xml +++ b/quickstep/recents_ui_overrides/res/values/colors.xml @@ -1,4 +1,9 @@ #fff + + #61000000 + #61FFFFFF + #3c000000 + #3cffffff \ No newline at end of file diff --git a/quickstep/recents_ui_overrides/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/dimens.xml index b654d5c90..f99143581 100644 --- a/quickstep/recents_ui_overrides/res/values/dimens.xml +++ b/quickstep/recents_ui_overrides/res/values/dimens.xml @@ -12,4 +12,13 @@ 4dp 10dp 14sp + + 17dp + 16dp + 8dp + 14sp + 8dp + + 2dp + \ No newline at end of file diff --git a/quickstep/recents_ui_overrides/res/values/override.xml b/quickstep/recents_ui_overrides/res/values/override.xml index c60cf5ab6..1ddd3f594 100644 --- a/quickstep/recents_ui_overrides/res/values/override.xml +++ b/quickstep/recents_ui_overrides/res/values/override.xml @@ -21,6 +21,8 @@ com.android.quickstep.InstantAppResolverImpl + com.android.launcher3.appprediction.PredictionAppTracker + com.android.quickstep.QuickstepProcessInitializer diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java new file mode 100644 index 000000000..c5c4add6b --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java @@ -0,0 +1,34 @@ +/* + * 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; + +import com.android.launcher3.appprediction.PredictionUiStateManager; +import com.android.launcher3.appprediction.PredictionUiStateManager.Client; + +import java.util.function.BiPredicate; + +public class LauncherInitListenerEx extends LauncherInitListener { + + public LauncherInitListenerEx(BiPredicate onInitListener) { + super(onInitListener); + } + + @Override + protected boolean init(Launcher launcher, boolean alreadyOnHome) { + PredictionUiStateManager.INSTANCE.get(launcher).switchClient(Client.OVERVIEW); + return super.init(launcher, alreadyOnHome); + } +} diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java new file mode 100644 index 000000000..948f39e0f --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java @@ -0,0 +1,207 @@ +/** + * 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.appprediction; + +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.quickstep.logging.UserEventDispatcherExtension.ALL_APPS_PREDICTION_TIPS; + +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.CornerPathEffect; +import android.graphics.Paint; +import android.graphics.drawable.ShapeDrawable; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.R; +import com.android.launcher3.allapps.FloatingHeaderView; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.compat.UserManagerCompat; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.graphics.TriangleShape; +import com.android.systemui.shared.system.LauncherEventUtil; + +import androidx.core.content.ContextCompat; + +/** + * All apps tip view aligned just above prediction apps, shown to users that enter all apps for the + * first time. + */ +public class AllAppsTipView extends AbstractFloatingView { + + private static final String ALL_APPS_TIP_SEEN = "launcher.all_apps_tip_seen"; + private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000; + private static final long SHOW_DELAY_MS = 200; + private static final long SHOW_DURATION_MS = 300; + private static final long HIDE_DURATION_MS = 100; + + private final Launcher mLauncher; + private final Handler mHandler = new Handler(); + + private AllAppsTipView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + private AllAppsTipView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setOrientation(LinearLayout.VERTICAL); + + mLauncher = Launcher.getLauncher(context); + + init(context); + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + close(true); + } + return false; + } + + @Override + protected void handleClose(boolean animate) { + if (mIsOpen) { + if (animate) { + animate().alpha(0f) + .withLayer() + .setStartDelay(0) + .setDuration(HIDE_DURATION_MS) + .setInterpolator(Interpolators.ACCEL) + .withEndAction(() -> mLauncher.getDragLayer().removeView(this)) + .start(); + } else { + animate().cancel(); + mLauncher.getDragLayer().removeView(this); + } + mLauncher.getSharedPrefs().edit().putBoolean(ALL_APPS_TIP_SEEN, true).apply(); + mIsOpen = false; + } + } + + @Override + public void logActionCommand(int command) { + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_ON_BOARD_POPUP) != 0; + } + + private void init(Context context) { + inflate(context, R.layout.arrow_toast, this); + + TextView textView = findViewById(R.id.text); + textView.setText(R.string.all_apps_prediction_tip); + + View dismissButton = findViewById(R.id.dismiss); + dismissButton.setOnClickListener(view -> { + mLauncher.getUserEventDispatcher().logActionTip( + LauncherEventUtil.DISMISS, ALL_APPS_PREDICTION_TIPS); + handleClose(true); + }); + + View arrowView = findViewById(R.id.arrow); + ViewGroup.LayoutParams arrowLp = arrowView.getLayoutParams(); + ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create( + arrowLp.width, arrowLp.height, false)); + Paint arrowPaint = arrowDrawable.getPaint(); + TypedValue typedValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true); + arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId)); + // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable. + arrowPaint.setPathEffect(new CornerPathEffect( + context.getResources().getDimension(R.dimen.arrow_toast_corner_radius))); + arrowView.setBackground(arrowDrawable); + + mIsOpen = true; + + mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS); + } + + private static boolean showAllAppsTipIfNecessary(Launcher launcher) { + FloatingHeaderView floatingHeaderView = launcher.getAppsView().getFloatingHeaderView(); + if (!floatingHeaderView.hasVisibleContent() + || AbstractFloatingView.getOpenView(launcher, + TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE) != null + || !launcher.isInState(ALL_APPS) + || hasSeenAllAppsTip(launcher) + || UserManagerCompat.getInstance(launcher).isDemoUser() + || ActivityManager.isRunningInTestHarness()) { + return false; + } + + AllAppsTipView allAppsTipView = new AllAppsTipView(launcher.getAppsView().getContext(), + null); + launcher.getDragLayer().addView(allAppsTipView); + + DragLayer.LayoutParams params = (DragLayer.LayoutParams) allAppsTipView.getLayoutParams(); + params.gravity = Gravity.CENTER_HORIZONTAL; + + int top = floatingHeaderView.findFixedRowByType(PredictionRowView.class).getTop(); + allAppsTipView.setY(top - launcher.getResources().getDimensionPixelSize( + R.dimen.all_apps_tip_bottom_margin)); + + allAppsTipView.setAlpha(0); + allAppsTipView.animate() + .alpha(1f) + .withLayer() + .setStartDelay(SHOW_DELAY_MS) + .setDuration(SHOW_DURATION_MS) + .setInterpolator(Interpolators.DEACCEL) + .start(); + + launcher.getUserEventDispatcher().logActionTip( + LauncherEventUtil.VISIBLE, ALL_APPS_PREDICTION_TIPS); + return true; + } + + private static boolean hasSeenAllAppsTip(Launcher launcher) { + return launcher.getSharedPrefs().getBoolean(ALL_APPS_TIP_SEEN, false); + } + + public static void scheduleShowIfNeeded(Launcher launcher) { + if (!hasSeenAllAppsTip(launcher)) { + launcher.getStateManager().addStateListener( + new LauncherStateManager.StateListener() { + @Override + public void onStateTransitionStart(LauncherState toState) { + } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + if (finalState == ALL_APPS) { + if (showAllAppsTipIfNecessary(launcher)) { + launcher.getStateManager().removeStateListener(this); + } + } + } + }); + } + } +} diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java new file mode 100644 index 000000000..311db2193 --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java @@ -0,0 +1,308 @@ +/** + * 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.appprediction; + +import static com.android.launcher3.LauncherState.ALL_APPS; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.os.Build; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.Interpolator; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager; +import com.android.launcher3.R; +import com.android.launcher3.allapps.FloatingHeaderRow; +import com.android.launcher3.allapps.FloatingHeaderView; +import com.android.launcher3.anim.PropertySetter; +import com.android.launcher3.util.Themes; + +import androidx.annotation.ColorInt; +import androidx.core.content.ContextCompat; + +/** + * A view which shows a horizontal divider + */ +@TargetApi(Build.VERSION_CODES.O) +public class AppsDividerView extends View implements LauncherStateManager.StateListener, + FloatingHeaderRow { + + private static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count"; + private static final int SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT = 20; + + public enum DividerType { + NONE, + LINE, + ALL_APPS_LABEL + } + + private final Launcher mLauncher; + private final TextPaint mPaint = new TextPaint(); + private DividerType mDividerType = DividerType.NONE; + + private final @ColorInt int mStrokeColor; + private final @ColorInt int mAllAppsLabelTextColor; + + private Layout mAllAppsLabelLayout; + private boolean mShowAllAppsLabel; + + private FloatingHeaderView mParent; + private boolean mTabsHidden; + private FloatingHeaderRow[] mRows = FloatingHeaderRow.NO_ROWS; + + private boolean mIsScrolledOut = false; + + public AppsDividerView(Context context) { + this(context, null); + } + + public AppsDividerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AppsDividerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLauncher = Launcher.getLauncher(context); + + boolean isMainColorDark = Themes.getAttrBoolean(context, R.attr.isMainColorDark); + mPaint.setStrokeWidth(getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height)); + + mStrokeColor = ContextCompat.getColor(context, isMainColorDark + ? R.color.all_apps_prediction_row_separator_dark + : R.color.all_apps_prediction_row_separator); + + mAllAppsLabelTextColor = ContextCompat.getColor(context, isMainColorDark + ? R.color.all_apps_label_text_dark + : R.color.all_apps_label_text); + } + + public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) { + mParent = parent; + mTabsHidden = tabsHidden; + mRows = rows; + updateDividerType(); + } + + @Override + public int getExpectedHeight() { + return getPaddingTop() + getPaddingBottom(); + } + + @Override + public boolean shouldDraw() { + return mDividerType != DividerType.NONE; + } + + @Override + public boolean hasVisibleContent() { + return false; + } + + private void updateDividerType() { + final DividerType dividerType; + if (!mTabsHidden) { + dividerType = DividerType.NONE; + } else { + // Check how many sections above me. + int sectionCount = 0; + for (FloatingHeaderRow row : mRows) { + if (row == this) { + break; + } else if (row.shouldDraw()) { + sectionCount ++; + } + } + + if (mShowAllAppsLabel && sectionCount > 0) { + dividerType = DividerType.ALL_APPS_LABEL; + } else if (sectionCount == 1) { + dividerType = DividerType.LINE; + } else { + dividerType = DividerType.NONE; + } + } + + if (mDividerType != dividerType) { + mDividerType = dividerType; + int topPadding; + int bottomPadding; + switch (dividerType) { + case LINE: + topPadding = 0; + bottomPadding = getResources() + .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height); + mPaint.setColor(mStrokeColor); + break; + case ALL_APPS_LABEL: + topPadding = getAllAppsLabelLayout().getHeight() + getResources() + .getDimensionPixelSize(R.dimen.all_apps_label_top_padding); + bottomPadding = getResources() + .getDimensionPixelSize(R.dimen.all_apps_label_bottom_padding); + mPaint.setColor(mAllAppsLabelTextColor); + break; + case NONE: + default: + topPadding = bottomPadding = 0; + break; + } + setPadding(getPaddingLeft(), topPadding, getPaddingRight(), bottomPadding); + updateViewVisibility(); + invalidate(); + requestLayout(); + if (mParent != null) { + mParent.onHeightUpdated(); + } + } + } + + private void updateViewVisibility() { + setVisibility(mDividerType == DividerType.NONE + ? GONE + : (mIsScrolledOut ? INVISIBLE : VISIBLE)); + } + + @Override + protected void onDraw(Canvas canvas) { + if (mDividerType == DividerType.LINE) { + int side = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin); + int y = getHeight() - (getPaddingBottom() / 2); + int x1 = getPaddingLeft() + side; + int x2 = getWidth() - getPaddingRight() - side; + canvas.drawLine(x1, y, x2, y, mPaint); + } else if (mDividerType == DividerType.ALL_APPS_LABEL) { + Layout textLayout = getAllAppsLabelLayout(); + int x = getWidth() / 2 - textLayout.getWidth() / 2; + int y = getHeight() - getPaddingBottom() - textLayout.getHeight(); + canvas.translate(x, y); + textLayout.draw(canvas); + canvas.translate(-x, -y); + } + } + + private Layout getAllAppsLabelLayout() { + if (mAllAppsLabelLayout == null) { + mPaint.setAntiAlias(true); + mPaint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL)); + mPaint.setTextSize( + getResources().getDimensionPixelSize(R.dimen.all_apps_label_text_size)); + + CharSequence allAppsLabelText = getResources().getText(R.string.all_apps_label); + mAllAppsLabelLayout = StaticLayout.Builder.obtain( + allAppsLabelText, 0, allAppsLabelText.length(), mPaint, + Math.round(mPaint.measureText(allAppsLabelText.toString()))) + .setAlignment(Layout.Alignment.ALIGN_CENTER) + .setMaxLines(1) + .setIncludePad(true) + .build(); + } + return mAllAppsLabelLayout; + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), + getPaddingBottom() + getPaddingTop()); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (shouldShowAllAppsLabel()) { + mShowAllAppsLabel = true; + mLauncher.getStateManager().addStateListener(this); + updateDividerType(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mLauncher.getStateManager().removeStateListener(this); + } + + @Override + public void onStateTransitionStart(LauncherState toState) { } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + if (finalState == ALL_APPS) { + setAllAppsVisitedCount(getAllAppsVisitedCount() + 1); + } else { + if (mShowAllAppsLabel != shouldShowAllAppsLabel()) { + mShowAllAppsLabel = !mShowAllAppsLabel; + updateDividerType(); + } + + if (!mShowAllAppsLabel) { + mLauncher.getStateManager().removeStateListener(this); + } + } + } + + private void setAllAppsVisitedCount(int count) { + mLauncher.getSharedPrefs().edit().putInt(ALL_APPS_VISITED_COUNT, count).apply(); + } + + private int getAllAppsVisitedCount() { + return mLauncher.getSharedPrefs().getInt(ALL_APPS_VISITED_COUNT, 0); + } + + private boolean shouldShowAllAppsLabel() { + return getAllAppsVisitedCount() < SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT; + } + + @Override + public void setInsets(Rect insets, DeviceProfile grid) { + int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx + + grid.cellLayoutPaddingLeftRightPx; + setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom()); + } + + @Override + public void setContentVisibility(boolean hasHeaderExtra, boolean hasContent, + PropertySetter setter, Interpolator fadeInterpolator) { + // Don't use setViewAlpha as we want to control the visibility ourselves. + setter.setFloat(this, ALPHA, hasContent ? 1 : 0, fadeInterpolator); + } + + @Override + public void setVerticalScroll(int scroll, boolean isScrolledOut) { + setTranslationY(scroll); + mIsScrolledOut = isScrolledOut; + updateViewVisibility(); + } + + @Override + public Class getTypeClass() { + return AppsDividerView.class; + } +} diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java new file mode 100644 index 000000000..b9f4147f8 --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java @@ -0,0 +1,69 @@ +/** + * 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.appprediction; + +import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER; + +import android.content.Context; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.ItemInfoWithIcon; +import com.android.launcher3.allapps.AllAppsStore; +import com.android.launcher3.shortcuts.ShortcutKey; +import com.android.launcher3.util.ComponentKey; + +public class ComponentKeyMapper { + + protected final ComponentKey componentKey; + private final Context mContext; + private final DynamicItemCache mCache; + + public ComponentKeyMapper(Context context, ComponentKey key, DynamicItemCache cache) { + mContext = context; + componentKey = key; + mCache = cache; + } + + public String getPackage() { + return componentKey.componentName.getPackageName(); + } + + public String getComponentClass() { + return componentKey.componentName.getClassName(); + } + + public ComponentKey getComponentKey() { + return componentKey; + } + + @Override + public String toString() { + return componentKey.toString(); + } + + public ItemInfoWithIcon getApp(AllAppsStore store) { + AppInfo item = store.getApp(componentKey); + if (item != null) { + return item; + } else if (getComponentClass().equals(COMPONENT_CLASS_MARKER)) { + return mCache.getInstantApp(componentKey.componentName.getPackageName()); + } else if (componentKey instanceof ShortcutKey) { + return mCache.getShortcutInfo((ShortcutKey) componentKey); + } + return null; + } +} diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java new file mode 100644 index 000000000..4ecc39cf6 --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java @@ -0,0 +1,242 @@ +/** + * 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.appprediction; + +import static android.content.pm.PackageManager.MATCH_INSTANT; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherModel; +import com.android.launcher3.WorkspaceItemInfo; +import com.android.launcher3.icons.IconCache; +import com.android.launcher3.icons.LauncherIcons; +import com.android.launcher3.shortcuts.DeepShortcutManager; +import com.android.launcher3.shortcuts.ShortcutKey; +import com.android.launcher3.util.InstantAppResolver; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.annotation.WorkerThread; + +/** + * Utility class which loads and caches predicted items like instant apps and shortcuts, before + * they can be displayed on the UI + */ +public class DynamicItemCache { + + private static final String TAG = "DynamicItemCache"; + private static final boolean DEBUG = false; + private static final String DEFAULT_URL = "default-url"; + + private static final int BG_MSG_LOAD_SHORTCUTS = 1; + private static final int BG_MSG_LOAD_INSTANT_APPS = 2; + + private static final int UI_MSG_UPDATE_SHORTCUTS = 1; + private static final int UI_MSG_UPDATE_INSTANT_APPS = 2; + + private final Context mContext; + private final Handler mWorker; + private final Handler mUiHandler; + private final InstantAppResolver mInstantAppResolver; + private final Runnable mOnUpdateCallback; + + private final Map mShortcuts; + private final Map mInstantApps; + + public DynamicItemCache(Context context, Runnable onUpdateCallback) { + mContext = context; + mWorker = new Handler(LauncherModel.getWorkerLooper(), this::handleWorkerMessage); + mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage); + mInstantAppResolver = InstantAppResolver.newInstance(context); + mOnUpdateCallback = onUpdateCallback; + + mShortcuts = new HashMap<>(); + mInstantApps = new HashMap<>(); + } + + public void cacheItems(List shortcutKeys, List pkgNames) { + if (!shortcutKeys.isEmpty()) { + mWorker.removeMessages(BG_MSG_LOAD_SHORTCUTS); + Message.obtain(mWorker, BG_MSG_LOAD_SHORTCUTS, shortcutKeys).sendToTarget(); + } + if (!pkgNames.isEmpty()) { + mWorker.removeMessages(BG_MSG_LOAD_INSTANT_APPS); + Message.obtain(mWorker, BG_MSG_LOAD_INSTANT_APPS, pkgNames).sendToTarget(); + } + } + + private boolean handleWorkerMessage(Message msg) { + switch (msg.what) { + case BG_MSG_LOAD_SHORTCUTS: { + List shortcutKeys = msg.obj != null ? + (List) msg.obj : Collections.EMPTY_LIST; + Map shortcutKeyAndInfos = new ArrayMap<>(); + for (ShortcutKey shortcutKey : shortcutKeys) { + WorkspaceItemInfo workspaceItemInfo = loadShortcutWorker(shortcutKey); + if (workspaceItemInfo != null) { + shortcutKeyAndInfos.put(shortcutKey, workspaceItemInfo); + } + } + Message.obtain(mUiHandler, UI_MSG_UPDATE_SHORTCUTS, shortcutKeyAndInfos) + .sendToTarget(); + return true; + } + case BG_MSG_LOAD_INSTANT_APPS: { + List pkgNames = msg.obj != null ? + (List) msg.obj : Collections.EMPTY_LIST; + List instantAppItemInfos = new ArrayList<>(); + for (String pkgName : pkgNames) { + InstantAppItemInfo instantAppItemInfo = loadInstantApp(pkgName); + if (instantAppItemInfo != null) { + instantAppItemInfos.add(instantAppItemInfo); + } + } + Message.obtain(mUiHandler, UI_MSG_UPDATE_INSTANT_APPS, instantAppItemInfos) + .sendToTarget(); + return true; + } + } + + return false; + } + + private boolean handleUiMessage(Message msg) { + switch (msg.what) { + case UI_MSG_UPDATE_SHORTCUTS: { + mShortcuts.clear(); + mShortcuts.putAll((Map) msg.obj); + mOnUpdateCallback.run(); + return true; + } + case UI_MSG_UPDATE_INSTANT_APPS: { + List instantAppItemInfos = (List) msg.obj; + mInstantApps.clear(); + for (InstantAppItemInfo instantAppItemInfo : instantAppItemInfos) { + mInstantApps.put(instantAppItemInfo.getTargetComponent().getPackageName(), + instantAppItemInfo); + } + mOnUpdateCallback.run(); + if (DEBUG) { + Log.d(TAG, String.format("Cache size: %d, Cache: %s", + mInstantApps.size(), mInstantApps.toString())); + } + return true; + } + } + + return false; + } + + @WorkerThread + private WorkspaceItemInfo loadShortcutWorker(ShortcutKey shortcutKey) { + DeepShortcutManager mgr = DeepShortcutManager.getInstance(mContext); + List details = mgr.queryForFullDetails( + shortcutKey.componentName.getPackageName(), + Collections.singletonList(shortcutKey.getId()), + shortcutKey.user); + if (!details.isEmpty()) { + WorkspaceItemInfo si = new WorkspaceItemInfo(details.get(0), mContext); + try (LauncherIcons li = LauncherIcons.obtain(mContext)) { + si.applyFrom(li.createShortcutIcon(details.get(0), true /* badged */, null)); + } catch (Exception e) { + if (DEBUG) { + Log.e(TAG, "Error loading shortcut icon for " + shortcutKey.toString()); + } + return null; + } + return si; + } + if (DEBUG) { + Log.d(TAG, "No shortcut found: " + shortcutKey.toString()); + } + return null; + } + + private InstantAppItemInfo loadInstantApp(String pkgName) { + PackageManager pm = mContext.getPackageManager(); + + try { + ApplicationInfo ai = pm.getApplicationInfo(pkgName, 0); + if (!mInstantAppResolver.isInstantApp(ai)) { + return null; + } + } catch (PackageManager.NameNotFoundException e) { + return null; + } + + String url = retrieveDefaultUrl(pkgName, pm); + if (url == null) { + Log.w(TAG, "no default-url available for pkg " + pkgName); + return null; + } + + Intent intent = new Intent(Intent.ACTION_VIEW) + .addCategory(Intent.CATEGORY_BROWSABLE) + .setData(Uri.parse(url)); + InstantAppItemInfo info = new InstantAppItemInfo(intent, pkgName); + IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache(); + iconCache.getTitleAndIcon(info, false); + if (info.iconBitmap == null || iconCache.isDefaultIcon(info.iconBitmap, info.user)) { + return null; + } + return info; + } + + @Nullable + public static String retrieveDefaultUrl(String pkgName, PackageManager pm) { + Intent mainIntent = new Intent().setAction(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER).setPackage(pkgName); + List resolveInfos = pm.queryIntentActivities( + mainIntent, MATCH_INSTANT | PackageManager.GET_META_DATA); + String url = null; + for (ResolveInfo resolveInfo : resolveInfos) { + if (resolveInfo.activityInfo.metaData != null + && resolveInfo.activityInfo.metaData.containsKey(DEFAULT_URL)) { + url = resolveInfo.activityInfo.metaData.getString(DEFAULT_URL); + } + } + return url; + } + + @UiThread + public InstantAppItemInfo getInstantApp(String pkgName) { + return mInstantApps.get(pkgName); + } + + @MainThread + public WorkspaceItemInfo getShortcutInfo(ShortcutKey key) { + return mShortcuts.get(key); + } +} diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java new file mode 100644 index 000000000..6e5f4617f --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/InstantAppItemInfo.java @@ -0,0 +1,50 @@ +/** + * 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.appprediction; + +import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER; + +import android.content.ComponentName; +import android.content.Intent; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.WorkspaceItemInfo; + +public class InstantAppItemInfo extends AppInfo { + + public InstantAppItemInfo(Intent intent, String packageName) { + this.intent = intent; + this.componentName = new ComponentName(packageName, COMPONENT_CLASS_MARKER); + } + + @Override + public ComponentName getTargetComponent() { + return componentName; + } + + @Override + public WorkspaceItemInfo makeWorkspaceItem() { + WorkspaceItemInfo workspaceItemInfo = super.makeWorkspaceItem(); + workspaceItemInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; + workspaceItemInfo.status = WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON + | WorkspaceItemInfo.FLAG_RESTORE_STARTED + | WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI; + workspaceItemInfo.intent.setPackage(componentName.getPackageName()); + return workspaceItemInfo; + } +} diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java new file mode 100644 index 000000000..bd7857330 --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java @@ -0,0 +1,214 @@ +/** + * 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.appprediction; + +import static com.android.launcher3.appprediction.PredictionUiStateManager.KEY_APP_SUGGESTION; + +import android.annotation.TargetApi; +import android.app.prediction.AppPredictionContext; +import android.app.prediction.AppPredictionManager; +import android.app.prediction.AppPredictor; +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetEvent; +import android.app.prediction.AppTargetId; +import android.content.ComponentName; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.os.UserHandle; +import android.util.Log; + +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.Utilities; +import com.android.launcher3.model.AppLaunchTracker; +import com.android.launcher3.util.UiThreadHelper; + +import com.android.launcher3.appprediction.PredictionUiStateManager.Client; + +import androidx.annotation.UiThread; +import androidx.annotation.WorkerThread; + +/** + * Subclass of app tracker which publishes the data to the prediction engine and gets back results. + */ +@TargetApi(Build.VERSION_CODES.Q) +public class PredictionAppTracker extends AppLaunchTracker + implements OnSharedPreferenceChangeListener { + + private static final String TAG = "PredictionAppTracker"; + private static final boolean DBG = false; + + private static final int MSG_INIT = 0; + private static final int MSG_DESTROY = 1; + private static final int MSG_LAUNCH = 2; + private static final int MSG_PREDICT = 3; + + private final Context mContext; + private final Handler mMessageHandler; + + private boolean mEnabled; + + // Accessed only on worker thread + private AppPredictor mHomeAppPredictor; + private AppPredictor mRecentsOverviewPredictor; + + public PredictionAppTracker(Context context) { + mContext = context; + mMessageHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleMessage); + + SharedPreferences prefs = Utilities.getPrefs(context); + setEnabled(prefs.getBoolean(KEY_APP_SUGGESTION, true)); + prefs.registerOnSharedPreferenceChangeListener(this); + InvariantDeviceProfile.INSTANCE.get(mContext).addOnChangeListener(this::onIdpChanged); + } + + @UiThread + private void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) { + // Reinitialize everything + setEnabled(mEnabled); + } + + @Override + @UiThread + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + if (KEY_APP_SUGGESTION.equals(key)) { + setEnabled(prefs.getBoolean(KEY_APP_SUGGESTION, true)); + } + } + + @WorkerThread + private void destroy() { + if (mHomeAppPredictor != null) { + mHomeAppPredictor.destroy(); + mHomeAppPredictor = null; + } + if (mRecentsOverviewPredictor != null) { + mRecentsOverviewPredictor.destroy(); + mRecentsOverviewPredictor = null; + } + } + + @WorkerThread + private AppPredictor createPredictor(Client client, int count) { + AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class); + + AppPredictor predictor = apm.createAppPredictionSession( + new AppPredictionContext.Builder(mContext) + .setUiSurface(client.id) + .setPredictedTargetCount(count) + .build()); + predictor.registerPredictionUpdates(mContext.getMainExecutor(), + PredictionUiStateManager.INSTANCE.get(mContext).appPredictorCallback(client)); + predictor.requestPredictionUpdate(); + return predictor; + } + + @WorkerThread + private boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_INIT: { + // Destroy any existing clients + destroy(); + + // Initialize the clients + int count = InvariantDeviceProfile.INSTANCE.get(mContext).numColumns; + mHomeAppPredictor = createPredictor(Client.HOME, count); + mRecentsOverviewPredictor = createPredictor(Client.OVERVIEW, count); + return true; + } + case MSG_DESTROY: { + destroy(); + return true; + } + case MSG_LAUNCH: { + if (mEnabled && mHomeAppPredictor != null) { + mHomeAppPredictor.notifyAppTargetEvent((AppTargetEvent) msg.obj); + } + return true; + } + case MSG_PREDICT: { + if (mEnabled && mHomeAppPredictor != null) { + String client = (String) msg.obj; + if (Client.HOME.id.equals(client)) { + mHomeAppPredictor.requestPredictionUpdate(); + } else { + mRecentsOverviewPredictor.requestPredictionUpdate(); + } + } + return true; + } + } + return false; + } + + @Override + @UiThread + public void onReturnedToHome() { + String client = Client.HOME.id; + mMessageHandler.removeMessages(MSG_PREDICT, client); + Message.obtain(mMessageHandler, MSG_PREDICT, client).sendToTarget(); + if (DBG) { + Log.d(TAG, String.format("Sent immediate message to update %s", client)); + } + } + + @UiThread + public void setEnabled(boolean isEnabled) { + mEnabled = isEnabled; + if (isEnabled) { + mMessageHandler.removeMessages(MSG_DESTROY); + mMessageHandler.sendEmptyMessage(MSG_INIT); + } else { + mMessageHandler.removeMessages(MSG_INIT); + mMessageHandler.sendEmptyMessage(MSG_DESTROY); + } + } + + @Override + @UiThread + public void onStartShortcut(String packageName, String shortcutId, UserHandle user, + String container) { + // TODO: Use the full shortcut info + AppTarget target = new AppTarget.Builder(new AppTargetId("shortcut:" + shortcutId)) + .setTarget(packageName, user) + .setClassName(shortcutId) + .build(); + sendLaunch(target, container); + } + + @Override + @UiThread + public void onStartApp(ComponentName cn, UserHandle user, String container) { + if (cn != null) { + AppTarget target = new AppTarget.Builder(new AppTargetId("app:" + cn)) + .setTarget(cn.getPackageName(), user) + .setClassName(cn.getClassName()) + .build(); + sendLaunch(target, container); + } + } + + @UiThread + private void sendLaunch(AppTarget target, String container) { + AppTargetEvent event = new AppTargetEvent.Builder(target, AppTargetEvent.ACTION_LAUNCH) + .setLaunchLocation(container == null ? CONTAINER_DEFAULT : container) + .build(); + Message.obtain(mMessageHandler, MSG_LAUNCH, event).sendToTarget(); + } +} diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java new file mode 100644 index 000000000..55f4c98e9 --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java @@ -0,0 +1,411 @@ +/** + * 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.appprediction; + +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.util.IntProperty; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.Interpolator; +import android.widget.LinearLayout; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.ItemInfoWithIcon; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.R; +import com.android.launcher3.WorkspaceItemInfo; +import com.android.launcher3.allapps.AllAppsStore; +import com.android.launcher3.allapps.FloatingHeaderRow; +import com.android.launcher3.allapps.FloatingHeaderView; +import com.android.launcher3.anim.AlphaUpdateListener; +import com.android.launcher3.anim.PropertySetter; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.keyboard.FocusIndicatorHelper; +import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper; +import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider; +import com.android.launcher3.model.AppLaunchTracker; +import com.android.launcher3.touch.ItemClickHandler; +import com.android.launcher3.touch.ItemLongClickListener; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.util.Themes; +import com.android.quickstep.AnimatedFloat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +@TargetApi(Build.VERSION_CODES.P) +public class PredictionRowView extends LinearLayout implements + LogContainerProvider, OnDeviceProfileChangeListener, FloatingHeaderRow { + + private static final String TAG = "PredictionRowView"; + + private static final IntProperty TEXT_ALPHA = + new IntProperty("textAlpha") { + @Override + public void setValue(PredictionRowView view, int alpha) { + view.setTextAlpha(alpha); + } + + @Override + public Integer get(PredictionRowView view) { + return view.mIconCurrentTextAlpha; + } + }; + + private static final Interpolator ALPHA_FACTOR_INTERPOLATOR = + (t) -> (t < 0.8f) ? 0 : (t - 0.8f) / 0.2f; + + private static final OnClickListener PREDICTION_CLICK_LISTENER = + ItemClickHandler.getInstance(AppLaunchTracker.CONTAINER_PREDICTIONS); + + private final Launcher mLauncher; + private final PredictionUiStateManager mPredictionUiStateManager; + private final int mNumPredictedAppsPerRow; + + // The set of predicted app component names + private final List mPredictedAppComponents = new ArrayList<>(); + // The set of predicted apps resolved from the component names and the current set of apps + private final ArrayList mPredictedApps = new ArrayList<>(); + // Helper to drawing the focus indicator. + private final FocusIndicatorHelper mFocusHelper; + + private final int mIconTextColor; + private final int mIconFullTextAlpha; + private int mIconCurrentTextAlpha; + + private FloatingHeaderView mParent; + private boolean mScrolledOut; + + private float mScrollTranslation = 0; + private final AnimatedFloat mContentAlphaFactor = + new AnimatedFloat(this::updateTranslationAndAlpha); + private final AnimatedFloat mOverviewScrollFactor = + new AnimatedFloat(this::updateTranslationAndAlpha); + + private View mLoadingProgress; + + private boolean mPredictionsEnabled = false; + + public PredictionRowView(@NonNull Context context) { + this(context, null); + } + + public PredictionRowView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + setOrientation(LinearLayout.HORIZONTAL); + + mFocusHelper = new SimpleFocusIndicatorHelper(this); + + mNumPredictedAppsPerRow = LauncherAppState.getIDP(context).numColumns; + mLauncher = Launcher.getLauncher(context); + mLauncher.addOnDeviceProfileChangeListener(this); + + mPredictionUiStateManager = PredictionUiStateManager.INSTANCE.get(context); + + mIconTextColor = Themes.getAttrColor(context, android.R.attr.textColorSecondary); + mIconFullTextAlpha = Color.alpha(mIconTextColor); + mIconCurrentTextAlpha = mIconFullTextAlpha; + + updateVisibility(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + mPredictionUiStateManager.setTargetAppsView(mLauncher.getAppsView()); + getAppsStore().registerIconContainer(this); + AllAppsTipView.scheduleShowIfNeeded(mLauncher); + } + + private AllAppsStore getAppsStore() { + return mLauncher.getAppsView().getAppsStore(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + mPredictionUiStateManager.setTargetAppsView(null); + getAppsStore().unregisterIconContainer(this); + } + + public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) { + mParent = parent; + setPredictionsEnabled(mPredictionUiStateManager.arePredictionsEnabled()); + } + + private void setPredictionsEnabled(boolean predictionsEnabled) { + if (predictionsEnabled != mPredictionsEnabled) { + mPredictionsEnabled = predictionsEnabled; + updateVisibility(); + } + } + + private void updateVisibility() { + setVisibility(mPredictionsEnabled ? VISIBLE : GONE); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getExpectedHeight(), + MeasureSpec.EXACTLY)); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + mFocusHelper.draw(canvas); + super.dispatchDraw(canvas); + } + + @Override + public int getExpectedHeight() { + return getVisibility() == GONE ? 0 : + Launcher.getLauncher(getContext()).getDeviceProfile().allAppsCellHeightPx + + getPaddingTop() + getPaddingBottom(); + } + + @Override + public boolean shouldDraw() { + return getVisibility() != GONE; + } + + @Override + public boolean hasVisibleContent() { + return mPredictionUiStateManager.arePredictionsEnabled(); + } + + /** + * Returns the predicted apps. + */ + public List getPredictedApps() { + return mPredictedApps; + } + + /** + * Sets the current set of predicted apps. + * + * This can be called before we get the full set of applications, we should merge the results + * only in onPredictionsUpdated() which is idempotent. + * + * If the number of predicted apps is the same as the previous list of predicted apps, + * we can optimize by swapping them in place. + */ + public void setPredictedApps(boolean predictionsEnabled, List apps) { + setPredictionsEnabled(predictionsEnabled); + mPredictedAppComponents.clear(); + mPredictedAppComponents.addAll(apps); + + mPredictedApps.clear(); + mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents)); + applyPredictionApps(); + } + + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + removeAllViews(); + applyPredictionApps(); + } + + private void applyPredictionApps() { + if (mLoadingProgress != null) { + removeView(mLoadingProgress); + } + if (!mPredictionsEnabled) { + mParent.onHeightUpdated(); + return; + } + + if (getChildCount() != mNumPredictedAppsPerRow) { + while (getChildCount() > mNumPredictedAppsPerRow) { + removeViewAt(0); + } + while (getChildCount() < mNumPredictedAppsPerRow) { + BubbleTextView icon = (BubbleTextView) mLauncher.getLayoutInflater().inflate( + R.layout.all_apps_icon, this, false); + icon.setOnClickListener(PREDICTION_CLICK_LISTENER); + icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_ALL_APPS); + icon.setLongPressTimeoutFactor(1f); + icon.setOnFocusChangeListener(mFocusHelper); + + LayoutParams lp = (LayoutParams) icon.getLayoutParams(); + // Ensure the all apps icon height matches the workspace icons in portrait mode. + lp.height = mLauncher.getDeviceProfile().allAppsCellHeightPx; + lp.width = 0; + lp.weight = 1; + addView(icon); + } + } + + int predictionCount = mPredictedApps.size(); + int iconColor = setColorAlphaBound(mIconTextColor, mIconCurrentTextAlpha); + + for (int i = 0; i < getChildCount(); i++) { + BubbleTextView icon = (BubbleTextView) getChildAt(i); + icon.reset(); + if (predictionCount > i) { + icon.setVisibility(View.VISIBLE); + if (mPredictedApps.get(i) instanceof AppInfo) { + icon.applyFromApplicationInfo((AppInfo) mPredictedApps.get(i)); + } else if (mPredictedApps.get(i) instanceof WorkspaceItemInfo) { + icon.applyFromWorkspaceItem((WorkspaceItemInfo) mPredictedApps.get(i)); + } + icon.setTextColor(iconColor); + } else { + icon.setVisibility(predictionCount == 0 ? GONE : INVISIBLE); + } + } + + if (predictionCount == 0) { + if (mLoadingProgress == null) { + mLoadingProgress = LayoutInflater.from(getContext()) + .inflate(R.layout.prediction_load_progress, this, false); + } + addView(mLoadingProgress); + } else { + mLoadingProgress = null; + } + + mParent.onHeightUpdated(); + } + + private List processPredictedAppComponents(List components) { + if (getAppsStore().getApps().isEmpty()) { + // Apps have not been bound yet. + return Collections.emptyList(); + } + + List predictedApps = new ArrayList<>(); + for (ComponentKeyMapper mapper : components) { + ItemInfoWithIcon info = mapper.getApp(getAppsStore()); + if (info != null) { + predictedApps.add(info); + } else { + if (FeatureFlags.IS_DOGFOOD_BUILD) { + Log.e(TAG, "Predicted app not found: " + mapper); + } + } + // Stop at the number of predicted apps + if (predictedApps.size() == mNumPredictedAppsPerRow) { + break; + } + } + return predictedApps; + } + + @Override + public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target, + LauncherLogProto.Target targetParent) { + for (int i = 0; i < mPredictedApps.size(); i++) { + ItemInfoWithIcon appInfo = mPredictedApps.get(i); + if (appInfo == info) { + targetParent.containerType = LauncherLogProto.ContainerType.PREDICTION; + target.predictedRank = i; + break; + } + } + } + + public void setTextAlpha(int alpha) { + mIconCurrentTextAlpha = alpha; + int iconColor = setColorAlphaBound(mIconTextColor, mIconCurrentTextAlpha); + + if (mLoadingProgress == null) { + for (int i = 0; i < getChildCount(); i++) { + ((BubbleTextView) getChildAt(i)).setTextColor(iconColor); + } + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + + @Override + public void setVerticalScroll(int scroll, boolean isScrolledOut) { + mScrolledOut = isScrolledOut; + updateTranslationAndAlpha(); + if (!isScrolledOut) { + mScrollTranslation = scroll; + updateTranslationAndAlpha(); + } + } + + private void updateTranslationAndAlpha() { + if (mPredictionsEnabled) { + setTranslationY((1 - mOverviewScrollFactor.value) * mScrollTranslation); + + float factor = ALPHA_FACTOR_INTERPOLATOR.getInterpolation(mOverviewScrollFactor.value); + float endAlpha = factor + (1 - factor) * (mScrolledOut ? 0 : 1); + setAlpha(mContentAlphaFactor.value * endAlpha); + AlphaUpdateListener.updateVisibility(this); + } + } + + @Override + public void setContentVisibility(boolean hasHeaderExtra, boolean hasContent, + PropertySetter setter, Interpolator fadeInterpolator) { + boolean isDrawn = getAlpha() > 0; + int textAlpha = hasHeaderExtra + ? (hasContent ? mIconFullTextAlpha : 0) // Text follows the content visibility + : mIconCurrentTextAlpha; // Leave as before + if (!isDrawn) { + // If the header is not drawn, no need to animate the text alpha + setTextAlpha(textAlpha); + } else { + setter.setInt(this, TEXT_ALPHA, textAlpha, fadeInterpolator); + } + + setter.setFloat(mOverviewScrollFactor, AnimatedFloat.VALUE, + (hasHeaderExtra && !hasContent) ? 1 : 0, LINEAR); + setter.setFloat(mContentAlphaFactor, AnimatedFloat.VALUE, hasHeaderExtra ? 1 : 0, + fadeInterpolator); + } + + @Override + public void setInsets(Rect insets, DeviceProfile grid) { + int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx + + grid.cellLayoutPaddingLeftRightPx; + setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom()); + } + + @Override + public Class getTypeClass() { + return PredictionRowView.class; + } +} diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java new file mode 100644 index 000000000..54fd84522 --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java @@ -0,0 +1,330 @@ +/** + * 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.appprediction; + +import static com.android.launcher3.LauncherState.BACKGROUND_APP; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER; + +import android.app.prediction.AppPredictor; +import android.app.prediction.AppTarget; +import android.content.ComponentName; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; + +import com.android.launcher3.AppInfo; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener; +import com.android.launcher3.ItemInfoWithIcon; +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.AllAppsContainerView; +import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener; +import com.android.launcher3.icons.IconCache; +import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; +import com.android.launcher3.shortcuts.ShortcutKey; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.MainThreadInitializedObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Handler responsible to updating the UI due to predicted apps changes. Operations: + * 1) Pushes the predicted apps to all-apps. If all-apps is visible, waits until it becomes + * invisible again before applying the changes. This ensures that the UI does not change abruptly + * in front of the user, even if an app launched and user pressed back button to return to the + * all-apps UI again. + * 2) Prefetch high-res icons for predicted apps. This ensures that we have the icons in memory + * even if all-apps is not opened as they are shown in search UI as well + * 3) Load instant app if it is not already in memory. As predictions are persisted on disk, + * instant app will not be in memory when launcher starts. + * 4) Maintains the current active client id (for the predictions) and all updates are performed on + * that client id. + */ +public class PredictionUiStateManager implements OnGlobalLayoutListener, ItemInfoUpdateReceiver, + OnSharedPreferenceChangeListener, OnIDPChangeListener, OnUpdateListener { + + public static final String KEY_APP_SUGGESTION = "pref_show_predictions"; + + // TODO (b/129421797): Update the client constants + public enum Client { + HOME("GEL"), + OVERVIEW("OVERVIEW_GEL"); + + public final String id; + + Client(String id) { + this.id = id; + } + } + + public static final MainThreadInitializedObject INSTANCE = + new MainThreadInitializedObject<>(PredictionUiStateManager::new); + + private final Context mContext; + private final SharedPreferences mMainPrefs; + + private final DynamicItemCache mDynamicItemCache; + private final List[] mPredictionServicePredictions; + + private int mMaxIconsPerRow; + private Client mActiveClient; + + private AllAppsContainerView mAppsView; + + private PredictionState mPendingState; + private PredictionState mCurrentState; + + private PredictionUiStateManager(Context context) { + mContext = context; + mMainPrefs = Utilities.getPrefs(context); + + mDynamicItemCache = new DynamicItemCache(context, this::onAppsUpdated); + + mActiveClient = Client.HOME; + + InvariantDeviceProfile idp = LauncherAppState.getIDP(context); + mMaxIconsPerRow = idp.numColumns; + + idp.addOnChangeListener(this); + mPredictionServicePredictions = new List[Client.values().length]; + for (int i = 0; i < mPredictionServicePredictions.length; i++) { + mPredictionServicePredictions[i] = Collections.emptyList(); + } + // Listens for enable/disable signal, and predictions if using AiAi is disabled. + mMainPrefs.registerOnSharedPreferenceChangeListener(this); + // Call this last + mCurrentState = parseLastState(); + } + + @Override + public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) { + mMaxIconsPerRow = profile.numColumns; + } + + public Client getClient() { + return mActiveClient; + } + + public void switchClient(Client client) { + if (client == mActiveClient) { + return; + } + mActiveClient = client; + dispatchOnChange(true); + } + + public void setTargetAppsView(AllAppsContainerView appsView) { + if (mAppsView != null) { + mAppsView.getAppsStore().removeUpdateListener(this); + } + mAppsView = appsView; + if (mAppsView != null) { + mAppsView.getAppsStore().addUpdateListener(this); + } + if (mPendingState != null) { + applyState(mPendingState); + mPendingState = null; + } else { + applyState(mCurrentState); + } + updateDependencies(mCurrentState); + } + + @Override + public void reapplyItemInfo(ItemInfoWithIcon info) { } + + @Override + public void onGlobalLayout() { + if (mAppsView == null) { + return; + } + if (mPendingState != null && canApplyPredictions(mPendingState)) { + applyState(mPendingState); + mPendingState = null; + } + if (mPendingState == null) { + mAppsView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + } + + private void scheduleApplyPredictedApps(PredictionState state) { + boolean registerListener = mPendingState == null; + mPendingState = state; + if (registerListener) { + // OnGlobalLayoutListener is called whenever a view in the view tree changes + // visibility. Add a listener and wait until appsView is invisible again. + mAppsView.getViewTreeObserver().addOnGlobalLayoutListener(this); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + if (KEY_APP_SUGGESTION.equals(key)) { + dispatchOnChange(true); + } + } + + private void applyState(PredictionState state) { + boolean wasEnabled = mCurrentState.isEnabled; + mCurrentState = state; + if (mAppsView != null) { + mAppsView.getFloatingHeaderView().findFixedRowByType(PredictionRowView.class) + .setPredictedApps(mCurrentState.isEnabled, mCurrentState.apps); + + if (wasEnabled != mCurrentState.isEnabled) { + // Reapply state as the State UI might have changed. + Launcher.getLauncher(mAppsView.getContext()).getStateManager().reapplyState(true); + } + } + } + + public AppPredictor.Callback appPredictorCallback(Client client) { + return targets -> { + mPredictionServicePredictions[client.ordinal()] = targets; + dispatchOnChange(true); + }; + } + + private void dispatchOnChange(boolean changed) { + PredictionState newState = changed ? parseLastState() : + (mPendingState == null ? mCurrentState : mPendingState); + if (changed && mAppsView != null && !canApplyPredictions(newState)) { + scheduleApplyPredictedApps(newState); + } else { + applyState(newState); + } + } + + private PredictionState parseLastState() { + PredictionState state = new PredictionState(); + state.isEnabled = mMainPrefs.getBoolean(KEY_APP_SUGGESTION, true); + if (!state.isEnabled) { + state.apps = Collections.EMPTY_LIST; + return state; + } + + state.apps = new ArrayList<>(); + + List appTargets = mPredictionServicePredictions[mActiveClient.ordinal()]; + if (!appTargets.isEmpty()) { + for (AppTarget appTarget : appTargets) { + ComponentKey key; + if (appTarget.getShortcutInfo() != null) { + key = ShortcutKey.fromInfo(appTarget.getShortcutInfo()); + } else { + key = new ComponentKey(new ComponentName(appTarget.getPackageName(), + appTarget.getClassName()), appTarget.getUser()); + } + state.apps.add(new ComponentKeyMapper(mContext, key, mDynamicItemCache)); + } + } + updateDependencies(state); + return state; + } + + private void updateDependencies(PredictionState state) { + if (!state.isEnabled || mAppsView == null) { + return; + } + + IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache(); + List instantAppsToLoad = new ArrayList<>(); + List shortcutsToLoad = new ArrayList<>(); + int total = state.apps.size(); + for (int i = 0, count = 0; i < total && count < mMaxIconsPerRow; i++) { + ComponentKeyMapper mapper = state.apps.get(i); + // Update instant apps + if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) { + instantAppsToLoad.add(mapper.getPackage()); + count++; + } else if (mapper.getComponentKey() instanceof ShortcutKey) { + shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey()); + count++; + } else { + // Reload high res icon + AppInfo info = (AppInfo) mapper.getApp(mAppsView.getAppsStore()); + if (info != null) { + if (info.usingLowResIcon()) { + // TODO: Update icon cache to support null callbacks. + iconCache.updateIconInBackground(this, info); + } + count++; + } + } + } + mDynamicItemCache.cacheItems(shortcutsToLoad, instantAppsToLoad); + } + + @Override + public void onAppsUpdated() { + dispatchOnChange(false); + } + + public boolean arePredictionsEnabled() { + return mCurrentState.isEnabled; + } + + private boolean canApplyPredictions(PredictionState newState) { + if (mAppsView == null) { + // If there is no apps view, no need to schedule. + return true; + } + Launcher launcher = Launcher.getLauncher(mAppsView.getContext()); + PredictionRowView predictionRow = mAppsView.getFloatingHeaderView(). + findFixedRowByType(PredictionRowView.class); + if (!predictionRow.isShown() || predictionRow.getAlpha() == 0 || + launcher.isForceInvisible()) { + return true; + } + + if (mCurrentState.isEnabled != newState.isEnabled + || mCurrentState.apps.isEmpty() != newState.apps.isEmpty()) { + // If the visibility of the prediction row is changing, apply immediately. + return true; + } + + if (launcher.getDeviceProfile().isVerticalBarLayout()) { + // If we are here & mAppsView.isShown() = true, we are probably in all-apps or mid way + return false; + } + if (!launcher.isInState(OVERVIEW) && !launcher.isInState(BACKGROUND_APP)) { + // Just a fallback as we dont need to apply instantly, if we are not in the swipe-up UI + return false; + } + + // Instead of checking against 1, we should check against (1 + delta), where delta accounts + // for the nav-bar height (as app icon can still be visible under the nav-bar). Checking + // against 1, keeps the logic simple :) + return launcher.getAllAppsController().getProgress() > 1; + } + + public PredictionState getCurrentState() { + return mCurrentState; + } + + public static class PredictionState { + + public boolean isEnabled; + public List apps; + } +} diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java index 3b2d66d64..35783b58a 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java @@ -45,7 +45,7 @@ import androidx.annotation.UiThread; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.LauncherInitListener; +import com.android.launcher3.LauncherInitListenerEx; import com.android.launcher3.LauncherState; import com.android.launcher3.allapps.DiscoveryBounce; import com.android.launcher3.anim.AnimatorPlaybackController; @@ -304,7 +304,7 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe @Override public ActivityInitListener createActivityInitListener( BiPredicate onInitListener) { - return new LauncherInitListener(onInitListener); + return new LauncherInitListenerEx(onInitListener); } @Nullable diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java index d979c991a..84af109a3 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java @@ -36,6 +36,8 @@ import java.util.List; */ public class TaskOverlayFactory implements ResourceBasedOverride { + public static final String AIAI_PACKAGE = "com.google.android.as"; + /** Note that these will be shown in order from top to bottom, if available for the task. */ private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[]{ new TaskSystemShortcut.AppInfo(), diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java index efe16c557..c31e82924 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java @@ -21,7 +21,6 @@ import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.QuickstepAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN; import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; -import static com.android.launcher3.config.FeatureFlags.ENABLE_HINTS_IN_OVERVIEW; import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import android.animation.AnimatorSet; @@ -39,7 +38,8 @@ import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.R; import com.android.launcher3.anim.Interpolators; -import com.android.launcher3.views.BaseDragLayer; +import com.android.launcher3.appprediction.PredictionUiStateManager; +import com.android.launcher3.appprediction.PredictionUiStateManager.Client; import com.android.launcher3.views.ScrimView; import com.android.quickstep.SysUINavigationMode; import com.android.quickstep.util.ClipAnimationHelper; @@ -195,4 +195,13 @@ public class LauncherRecentsView extends RecentsView { } } } + + @Override + public void reset() { + super.reset(); + + // We are moving to home or some other UI with no recents. Switch back to the home client, + // the home predictions should have been updated when the activity was resumed. + PredictionUiStateManager.INSTANCE.get(getContext()).switchClient(Client.HOME); + } } diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java index d8aeb35d6..72d60dae6 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java @@ -82,6 +82,8 @@ import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PropertyListBuilder; import com.android.launcher3.anim.SpringObjectAnimator; +import com.android.launcher3.appprediction.PredictionUiStateManager; +import com.android.launcher3.appprediction.PredictionUiStateManager.Client; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; @@ -751,8 +753,6 @@ public abstract class RecentsView extends PagedView impl unloadVisibleTaskData(); setCurrentPage(0); - - OverviewCallbacks.get(getContext()).onResetOverview(); } /** diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index 81565a572..4319b5d71 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -58,4 +58,13 @@ %1$s left today + + + App suggestions + + All apps + + Your predicted apps + + \ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/OverviewCallbacks.java b/quickstep/src/com/android/quickstep/OverviewCallbacks.java index ef9c5c0d9..f5573baa7 100644 --- a/quickstep/src/com/android/quickstep/OverviewCallbacks.java +++ b/quickstep/src/com/android/quickstep/OverviewCallbacks.java @@ -39,7 +39,5 @@ public class OverviewCallbacks implements ResourceBasedOverride { public void onInitOverviewTransition() { } - public void onResetOverview() { } - public void closeAllWindows() { } } diff --git a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java index 6dff187ea..ca7711f6b 100644 --- a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java +++ b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java @@ -36,6 +36,8 @@ import com.android.systemui.shared.system.MetricsLoggerCompat; @SuppressWarnings("unused") public class UserEventDispatcherExtension extends UserEventDispatcher { + public static final int ALL_APPS_PREDICTION_TIPS = 2; + private static final String TAG = "UserEventDispatcher"; public UserEventDispatcherExtension(Context context) { } diff --git a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java new file mode 100644 index 000000000..43f603940 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java @@ -0,0 +1,168 @@ +/** + * 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.quickstep; + +import static org.junit.Assert.assertEquals; + +import android.app.prediction.AppPredictor; +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetId; +import android.content.ComponentName; +import android.content.pm.LauncherActivityInfo; +import android.os.Process; +import android.view.View; +import android.widget.ProgressBar; + +import com.android.launcher3.BubbleTextView; +import com.android.launcher3.Launcher; +import com.android.launcher3.appprediction.PredictionRowView; +import com.android.launcher3.appprediction.PredictionUiStateManager; +import com.android.launcher3.appprediction.PredictionUiStateManager.Client; +import com.android.launcher3.compat.LauncherAppsCompat; +import com.android.launcher3.model.AppLaunchTracker; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; + +@LargeTest +@RunWith(AndroidJUnit4.class) +public class AppPredictionsUITests extends AbstractQuickStepTest { + private static final int DEFAULT_APP_LAUNCH_TIMES = 3; + private static final String TAG = "AppPredictionsUITests"; + + private LauncherActivityInfo mSampleApp1; + private LauncherActivityInfo mSampleApp2; + private LauncherActivityInfo mSampleApp3; + + private AppPredictor.Callback mCallback; + + @Before + public void setUp() throws Exception { + super.setUp(); + + List activities = LauncherAppsCompat.getInstance(mTargetContext) + .getActivityList(null, Process.myUserHandle()); + mSampleApp1 = activities.get(0); + mSampleApp2 = activities.get(1); + mSampleApp3 = activities.get(2); + + // Disable app tracker + AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker()); + + mCallback = PredictionUiStateManager.INSTANCE.get(mTargetContext).appPredictorCallback( + Client.HOME); + + mDevice.setOrientationNatural(); + } + + @After + public void tearDown() throws Throwable { + mDevice.unfreezeRotation(); + } + + /** + * Test that prediction UI is updated as soon as we get predictions from the system + */ + @Test + public void testPredictionExistsInAllApps() { + mActivityMonitor.startLauncher(); + mLauncher.pressHome().switchToAllApps(); + + // There has not been any update, verify that progress bar is showing + waitForLauncherCondition("Prediction is not in loading state", launcher -> { + ProgressBar p = findLoadingBar(launcher); + return p != null && p.isShown(); + }); + + // Dispatch an update + sendPredictionUpdate(mSampleApp1, mSampleApp2); + waitForLauncherCondition("Predictions were not updated in loading state", + launcher -> getPredictedApp(launcher).size() == 2); + } + + /** + * Test tat prediction update is deferred if it is already visible + */ + @Test + public void testPredictionsDeferredUntilHome() { + mActivityMonitor.startLauncher(); + sendPredictionUpdate(mSampleApp1, mSampleApp2); + mLauncher.pressHome().switchToAllApps(); + waitForLauncherCondition("Predictions were not updated in loading state", + launcher -> getPredictedApp(launcher).size() == 2); + + // Update predictions while all-apps is visible + sendPredictionUpdate(mSampleApp1, mSampleApp2, mSampleApp3); + assertEquals(2, getFromLauncher(this::getPredictedApp).size()); + + // Go home and go back to all-apps + mLauncher.pressHome().switchToAllApps(); + assertEquals(3, getFromLauncher(this::getPredictedApp).size()); + } + + public ArrayList getPredictedApp(Launcher launcher) { + PredictionRowView container = launcher.getAppsView().getFloatingHeaderView() + .findFixedRowByType(PredictionRowView.class); + + ArrayList predictedAppViews = new ArrayList<>(); + for (int i = 0; i < container.getChildCount(); i++) { + View view = container.getChildAt(i); + if (view instanceof BubbleTextView && view.getVisibility() == View.VISIBLE) { + predictedAppViews.add((BubbleTextView) view); + } + } + return predictedAppViews; + } + + private ProgressBar findLoadingBar(Launcher launcher) { + PredictionRowView container = launcher.getAppsView().getFloatingHeaderView() + .findFixedRowByType(PredictionRowView.class); + + for (int i = 0; i < container.getChildCount(); i++) { + View view = container.getChildAt(i); + if (view instanceof ProgressBar) { + return (ProgressBar) view; + } + } + return null; + } + + + private void sendPredictionUpdate(LauncherActivityInfo... activities) { + getOnUiThread(() -> { + List targets = new ArrayList<>(activities.length); + for (LauncherActivityInfo info : activities) { + ComponentName cn = info.getComponentName(); + AppTarget target = new AppTarget.Builder(new AppTargetId("app:" + cn)) + .setTarget(cn.getPackageName(), info.getUser()) + .setClassName(cn.getClassName()) + .build(); + targets.add(target); + } + mCallback.onTargetsAvailable(targets); + return null; + }); + } +} -- cgit v1.2.3