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:
@@ -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);
|
||||
}
|
||||
}
|
||||
+207
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+308
@@ -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;
|
||||
}
|
||||
}
|
||||
+69
@@ -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;
|
||||
}
|
||||
}
|
||||
+242
@@ -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);
|
||||
}
|
||||
}
|
||||
+50
@@ -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;
|
||||
}
|
||||
}
|
||||
+214
@@ -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();
|
||||
}
|
||||
}
|
||||
+411
@@ -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;
|
||||
}
|
||||
}
|
||||
+330
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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(),
|
||||
|
||||
+11
-2
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user