Merge "Adding support for showing predicted apps as a floating row in all-apps and overview" into ub-launcher3-qt-dev

am: b31b38c942

Change-Id: Id5c4e1b1e1732b517d6f5cde5a191f376cc16dc1
This commit is contained in:
Sunny Goyal
2019-04-23 15:03:52 -07:00
committed by android-build-merger
29 changed files with 2191 additions and 21 deletions
@@ -0,0 +1,19 @@
<!--
Copyright (C) 2018 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="?android:attr/colorAccent" />
<corners android:radius="8dp" />
</shape>
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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.
-->
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="4dp"
android:background="@drawable/arrow_toast_rounded_background"
android:layout_gravity="center_horizontal"
android:elevation="2dp"
android:orientation="horizontal">
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:textColor="@android:color/white"
android:textSize="16sp"/>
<ImageView
android:id="@+id/dismiss"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:padding="10dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:alpha="0.7"
android:src="@drawable/ic_remove_no_shadow"
android:tint="@android:color/white"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/accessibility_close_task"/>
</LinearLayout>
<View
android:id="@+id/arrow"
android:elevation="2dp"
android:layout_width="10dp"
android:layout_height="8dp"
android:layout_marginTop="-2dp"
android:layout_gravity="center_horizontal"/>
</merge>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<com.android.launcher3.appprediction.PredictionRowView
android:id="@+id/prediction_row"
android:accessibilityPaneTitle="@string/title_app_suggestions"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.android.launcher3.appprediction.AppsDividerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/apps_divider_view" />
</merge>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:indeterminate="true"
android:indeterminateOnly="true"
android:indeterminateTint="?workspaceTextColor" />
@@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="chip_hint_foreground_color">#fff</color>
<color name="all_apps_label_text">#61000000</color>
<color name="all_apps_label_text_dark">#61FFFFFF</color>
<color name="all_apps_prediction_row_separator">#3c000000</color>
<color name="all_apps_prediction_row_separator_dark">#3cffffff</color>
</resources>
@@ -12,4 +12,13 @@
<dimen name="chip_text_top_padding">4dp</dimen>
<dimen name="chip_text_start_padding">10dp</dimen>
<dimen name="chip_text_size">14sp</dimen>
<dimen name="all_apps_prediction_row_divider_height">17dp</dimen>
<dimen name="all_apps_label_top_padding">16dp</dimen>
<dimen name="all_apps_label_bottom_padding">8dp</dimen>
<dimen name="all_apps_label_text_size">14sp</dimen>
<dimen name="all_apps_tip_bottom_margin">8dp</dimen>
<!-- The size of corner radius of the arrow in the arrow toast. -->
<dimen name="arrow_toast_corner_radius">2dp</dimen>
</resources>
@@ -21,6 +21,8 @@
<string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
<string name="app_launch_tracker_class" translatable="false">com.android.launcher3.appprediction.PredictionAppTracker</string>
<string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
</resources>
@@ -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<Launcher, Boolean> onInitListener) {
super(onInitListener);
}
@Override
protected boolean init(Launcher launcher, boolean alreadyOnHome) {
PredictionUiStateManager.INSTANCE.get(launcher).switchClient(Client.OVERVIEW);
return super.init(launcher, alreadyOnHome);
}
}
@@ -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);
}
}
}
});
}
}
}
@@ -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<AppsDividerView> getTypeClass() {
return AppsDividerView.class;
}
}
@@ -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;
}
}
@@ -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<ShortcutKey, WorkspaceItemInfo> mShortcuts;
private final Map<String, InstantAppItemInfo> 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<ShortcutKey> shortcutKeys, List<String> 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<ShortcutKey> shortcutKeys = msg.obj != null ?
(List<ShortcutKey>) msg.obj : Collections.EMPTY_LIST;
Map<ShortcutKey, WorkspaceItemInfo> 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<String> pkgNames = msg.obj != null ?
(List<String>) msg.obj : Collections.EMPTY_LIST;
List<InstantAppItemInfo> 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<ShortcutKey, WorkspaceItemInfo>) msg.obj);
mOnUpdateCallback.run();
return true;
}
case UI_MSG_UPDATE_INSTANT_APPS: {
List<InstantAppItemInfo> instantAppItemInfos = (List<InstantAppItemInfo>) 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<ShortcutInfo> details = mgr.queryForFullDetails(
shortcutKey.componentName.getPackageName(),
Collections.<String>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<ResolveInfo> 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);
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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<PredictionRowView> TEXT_ALPHA =
new IntProperty<PredictionRowView>("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<ComponentKeyMapper> mPredictedAppComponents = new ArrayList<>();
// The set of predicted apps resolved from the component names and the current set of apps
private final ArrayList<ItemInfoWithIcon> 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<ItemInfoWithIcon> 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<ComponentKeyMapper> 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<ItemInfoWithIcon> processPredictedAppComponents(List<ComponentKeyMapper> components) {
if (getAppsStore().getApps().isEmpty()) {
// Apps have not been bound yet.
return Collections.emptyList();
}
List<ItemInfoWithIcon> 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<PredictionRowView> getTypeClass() {
return PredictionRowView.class;
}
}
@@ -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<PredictionUiStateManager> 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<AppTarget> 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<String> instantAppsToLoad = new ArrayList<>();
List<ShortcutKey> 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<ComponentKeyMapper> apps;
}
}
@@ -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<Launcher, Boolean> onInitListener) {
return new LauncherInitListener(onInitListener);
return new LauncherInitListenerEx(onInitListener);
}
@Nullable
@@ -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(),
@@ -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<Launcher> {
}
}
}
@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);
}
}
@@ -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<T extends BaseActivity> extends PagedView impl
unloadVisibleTaskData();
setCurrentPage(0);
OverviewCallbacks.get(getContext()).onResetOverview();
}
/**
+9
View File
@@ -58,4 +58,13 @@
<!-- Annotation shown on an app card in Recents, telling that the app has a usage limit set by
the user, and a given time is left for it today [CHAR LIMIT=22] -->
<string name="time_left_for_app"><xliff:g id="time" example="7 minutes">%1$s</xliff:g> left today</string>
<!-- Accessibility title for the row of all-apps containing app predictions. [CHAR LIMIT=50] -->
<string name="title_app_suggestions">App suggestions</string>
<!-- Label for the header text of the All Apps section in All Apps view, used to separate Predicted Apps and Actions section from All Apps section. [CHAR_LIMIT=50] -->
<string name="all_apps_label">All apps</string>
<!-- Text of the tip when user lands in all apps view for the first time, indicating where the tip toast points to is the predicted apps section. [CHAR_LIMIT=50] -->
<string name="all_apps_prediction_tip">Your predicted apps</string>
</resources>
@@ -39,7 +39,5 @@ public class OverviewCallbacks implements ResourceBasedOverride {
public void onInitOverviewTransition() { }
public void onResetOverview() { }
public void closeAllWindows() { }
}
@@ -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) { }
@@ -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<LauncherActivityInfo> 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<BubbleTextView> getPredictedApp(Launcher launcher) {
PredictionRowView container = launcher.getAppsView().getFloatingHeaderView()
.findFixedRowByType(PredictionRowView.class);
ArrayList<BubbleTextView> 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<AppTarget> 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;
});
}
}
-4
View File
@@ -2223,10 +2223,6 @@ public class Launcher extends BaseDraggingActivity implements LauncherExterns,
*/
public void bindAllApplications(ArrayList<AppInfo> apps) {
mAppsView.getAppsStore().setApps(apps);
if (mLauncherCallbacks != null) {
mLauncherCallbacks.bindAllApplications(apps);
}
}
/**
@@ -57,16 +57,10 @@ public interface LauncherCallbacks {
* Extension points for providing custom behavior on certain user interactions.
*/
void onLauncherProviderChange();
void bindAllApplications(ArrayList<AppInfo> apps);
/**
* Starts a search with {@param initialQuery}. Return false if search was not started.
*/
boolean startSearch(
String initialQuery, boolean selectInitialQuery, Bundle appSearchData);
/*
* Extensions points for adding / replacing some other aspects of the Launcher experience.
*/
boolean hasSettings();
}
@@ -22,6 +22,8 @@ import com.android.launcher3.MainThreadExecutor;
import java.util.concurrent.ExecutionException;
import androidx.annotation.VisibleForTesting;
/**
* Utility class for defining singletons which are initiated on main thread.
*/
@@ -53,6 +55,11 @@ public class MainThreadInitializedObject<T> {
return mValue;
}
@VisibleForTesting
public void initializeForTesting(T value) {
mValue = value;
}
public interface ObjectProvider<T> {
T get(Context context);
@@ -168,7 +168,7 @@ public abstract class AbstractLauncherUiTest {
}
@After
public void tearDown() {
public void verifyLauncherState() {
try {
// Limits UI tests affecting tests running after them.
waitForModelLoaded();
@@ -103,8 +103,6 @@ public class BindWidgetTest extends AbstractLauncherUiTest {
if (mSessionId > -1) {
mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
}
super.tearDown();
}
@Test