Show discovery tip for hybrid hotseat

Doc: go/hybrid-hotseat-tips

Issue 157683315: for fully populated hotseat, count returns to home screen and show discovery tip if Tip action was not tapped.
Issue 158301717: Don't use cached items if client has predicted items.

Test: Manual
Change-Id: I4747a1148caa62a6262fb6592d5185bdf216ede6
This commit is contained in:
Samuel Fufa 2020-05-28 16:55:26 -07:00
parent c11a808b99
commit 5b2da14e72
9 changed files with 102 additions and 43 deletions

View File

@ -47,12 +47,12 @@ public class HotseatEduController {
public static final String KEY_HOTSEAT_EDU_SEEN = "hotseat_edu_seen"; public static final String KEY_HOTSEAT_EDU_SEEN = "hotseat_edu_seen";
public static final String HOTSEAT_EDU_ACTION = public static final String HOTSEAT_EDU_ACTION =
"com.android.launcher3.action.SHOW_HYBRID_HOTSEAT_EDU"; "com.android.launcher3.action.SHOW_HYBRID_HOTSEAT_EDU";
private static final String SETTINGS_ACTION = public static final String SETTINGS_ACTION =
"android.settings.ACTION_CONTENT_SUGGESTIONS_SETTINGS"; "android.settings.ACTION_CONTENT_SUGGESTIONS_SETTINGS";
private final Launcher mLauncher; private final Launcher mLauncher;
private final Hotseat mHotseat; private final Hotseat mHotseat;
private final HotseatRestoreHelper mRestoreHelper; private HotseatRestoreHelper mRestoreHelper;
private List<WorkspaceItemInfo> mPredictedApps; private List<WorkspaceItemInfo> mPredictedApps;
private HotseatEduDialog mActiveDialog; private HotseatEduDialog mActiveDialog;
@ -71,14 +71,17 @@ public class HotseatEduController {
* Checks what type of migration should be used and migrates hotseat * Checks what type of migration should be used and migrates hotseat
*/ */
void migrate() { void migrate() {
mRestoreHelper.createBackup(); if (mRestoreHelper != null) {
mRestoreHelper.createBackup();
}
if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) { if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) {
migrateToFolder(); migrateToFolder();
} else { } else {
migrateHotseatWhole(); migrateHotseatWhole();
} }
Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_enabled, R.string.hotseat_turn_off, Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_enabled,
null, () -> mLauncher.startActivity(new Intent(SETTINGS_ACTION))); R.string.hotseat_prediction_settings, null,
() -> mLauncher.startActivity(new Intent(SETTINGS_ACTION)));
} }
/** /**
@ -223,15 +226,15 @@ public class HotseatEduController {
void finishOnboarding() { void finishOnboarding() {
mOnOnboardingComplete.run(); mOnOnboardingComplete.run();
destroy();
mLauncher.getSharedPrefs().edit().putBoolean(KEY_HOTSEAT_EDU_SEEN, true).apply(); mLauncher.getSharedPrefs().edit().putBoolean(KEY_HOTSEAT_EDU_SEEN, true).apply();
} }
void showDimissTip() { void showDimissTip() {
if (mHotseat.getShortcutsAndWidgets().getChildCount() if (mHotseat.getShortcutsAndWidgets().getChildCount()
< mLauncher.getDeviceProfile().inv.numHotseatIcons) { < mLauncher.getDeviceProfile().inv.numHotseatIcons) {
Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled, R.string.hotseat_turn_off, Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
null, () -> mLauncher.startActivity(new Intent(SETTINGS_ACTION))); R.string.hotseat_prediction_settings, null,
() -> mLauncher.startActivity(new Intent(SETTINGS_ACTION)));
} else { } else {
new ArrowTipView(mLauncher).show( new ArrowTipView(mLauncher).show(
mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop()); mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop());
@ -242,12 +245,6 @@ public class HotseatEduController {
mPredictedApps = predictedApps; mPredictedApps = predictedApps;
} }
void destroy() {
if (mActiveDialog != null) {
mActiveDialog.setHotseatEduController(null);
}
}
void showEdu() { void showEdu() {
int childCount = mHotseat.getShortcutsAndWidgets().getChildCount(); int childCount = mHotseat.getShortcutsAndWidgets().getChildCount();
CellLayout cellLayout = mLauncher.getWorkspace().getScreenWithId(Workspace.FIRST_SCREEN_ID); CellLayout cellLayout = mLauncher.getWorkspace().getScreenWithId(Workspace.FIRST_SCREEN_ID);

View File

@ -29,6 +29,7 @@ import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.CellLayout; import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Insettable; import com.android.launcher3.Insettable;
@ -245,6 +246,7 @@ public class HotseatEduDialog extends AbstractSlideInView implements Insettable
|| mHotseatEduController == null) { || mHotseatEduController == null) {
return; return;
} }
AbstractFloatingView.closeAllOpenViews(mLauncher);
attachToContainer(); attachToContainer();
logOnBoardingSeen(); logOnBoardingSeen();
animateOpen(); animateOpen();

View File

@ -17,6 +17,7 @@ package com.android.launcher3.hybridhotseat;
import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID; import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.hybridhotseat.HotseatEduController.SETTINGS_ACTION;
import android.animation.Animator; import android.animation.Animator;
import android.animation.AnimatorSet; import android.animation.AnimatorSet;
@ -27,6 +28,7 @@ import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget; import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent; import android.app.prediction.AppTargetEvent;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -64,6 +66,8 @@ import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.userevent.nano.LauncherLogProto;
import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntArray;
import com.android.launcher3.views.ArrowTipView;
import com.android.launcher3.views.Snackbar;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
@ -107,8 +111,6 @@ public class HotseatPredictionController implements DragController.DragListener,
private boolean mIsCacheEmpty; private boolean mIsCacheEmpty;
private boolean mIsDestroyed = false; private boolean mIsDestroyed = false;
private HotseatEduController mHotseatEduController;
private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>(); private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
@ -146,11 +148,48 @@ public class HotseatPredictionController implements DragController.DragListener,
} }
/** /**
* Transitions to NORMAL workspace mode and shows edu * Shows appropriate hotseat education based on prediction enabled and migration states.
*/ */
public void showEdu() { public void showEdu() {
if (mHotseatEduController == null) return; if (mComponentKeyMappers.isEmpty()) {
mHotseatEduController.showEdu(); // launcher has empty predictions set
Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_disabled,
R.string.hotseat_prediction_settings, null,
() -> mLauncher.startActivity(
new Intent(SETTINGS_ACTION)));
} else if (isEduSeen()) {
// user has already went through education
new ArrowTipView(mLauncher).show(
mLauncher.getString(R.string.hotsaet_tip_prediction_enabled),
mHotseat.getTop());
} else {
HotseatEduController eduController = new HotseatEduController(mLauncher, mRestoreHelper,
this::createPredictor);
eduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers));
eduController.showEdu();
}
}
/**
* Shows educational tip for hotseat if user does not go through Tips app.
*/
public void showDiscoveryTip() {
if (getPredictedIcons().size() == mHotSeatItemsCount) {
new ArrowTipView(mLauncher).show(
mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop());
} else {
Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
R.string.hotseat_prediction_settings, null,
() -> mLauncher.startActivity(new Intent(SETTINGS_ACTION)));
}
}
/**
* Returns if hotseat client has predictions
* @return
*/
public boolean hasPredictions() {
return !mComponentKeyMappers.isEmpty();
} }
@Override @Override
@ -250,10 +289,6 @@ public class HotseatPredictionController implements DragController.DragListener,
if (mAppPredictor != null) { if (mAppPredictor != null) {
mAppPredictor.destroy(); mAppPredictor.destroy();
} }
if (mHotseatEduController != null) {
mHotseatEduController.destroy();
mHotseatEduController = null;
}
} }
/** /**
@ -299,10 +334,6 @@ public class HotseatPredictionController implements DragController.DragListener,
mAppPredictor.requestPredictionUpdate(); mAppPredictor.requestPredictionUpdate();
}); });
setPauseUIUpdate(false); setPauseUIUpdate(false);
if (!isEduSeen()) {
mHotseatEduController = new HotseatEduController(mLauncher, mRestoreHelper,
this::createPredictor);
}
} }
/** /**
@ -350,9 +381,6 @@ public class HotseatPredictionController implements DragController.DragListener,
if (Utilities.IS_DEBUG_DEVICE) FileLog.d(TAG, predictionLog.toString()); if (Utilities.IS_DEBUG_DEVICE) FileLog.d(TAG, predictionLog.toString());
updateDependencies(); updateDependencies();
fillGapsWithPrediction(); fillGapsWithPrediction();
if (!isEduSeen() && mHotseatEduController != null) {
mHotseatEduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers));
}
cachePredictionComponentKeysIfNecessary(componentKeys); cachePredictionComponentKeysIfNecessary(componentKeys);
} }

View File

@ -88,7 +88,6 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
*/ */
public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) -> public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) ->
SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2); SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2);
private HotseatPredictionController mHotseatPredictionController;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -168,13 +167,6 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
} }
} }
/**
* Returns Prediction controller for hybrid hotseat
*/
public HotseatPredictionController getHotseatPredictionController() {
return mHotseatPredictionController;
}
/** /**
* Recents logic that triggers when launcher state changes or launcher activity stops/resumes. * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
*/ */
@ -195,7 +187,8 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
@Override @Override
public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) { public void bindPredictedItems(List<AppInfo> appInfos, IntArray ranks) {
super.bindPredictedItems(appInfos, ranks); super.bindPredictedItems(appInfos, ranks);
if (mHotseatPredictionController != null) { if (mHotseatPredictionController != null
&& !mHotseatPredictionController.hasPredictions()) {
mHotseatPredictionController.showCachedItems(appInfos, ranks); mHotseatPredictionController.showCachedItems(appInfos, ranks);
} }
} }

View File

@ -76,8 +76,8 @@
<!-- Button text to dismiss opt in for fully predicted hotseat --> <!-- Button text to dismiss opt in for fully predicted hotseat -->
<string name="hotseat_edu_dismiss">No thanks</string> <string name="hotseat_edu_dismiss">No thanks</string>
<!-- action shown to turn off predictions after onboarding --> <!-- action shown to toggle predictions after onboarding -->
<string name="hotseat_turn_off">Settings</string> <string name="hotseat_prediction_settings">Settings</string>
<!-- tip shown if user has no items in hotseat to migrate --> <!-- tip shown if user has no items in hotseat to migrate -->
<string name="hotseat_auto_enrolled">Most-used apps appear here, and change based on routines</string> <string name="hotseat_auto_enrolled">Most-used apps appear here, and change based on routines</string>
@ -86,7 +86,9 @@
<!-- tip shown if user declines migration and has some open spots for prediction --> <!-- tip shown if user declines migration and has some open spots for prediction -->
<string name="hotseat_tip_gaps_filled">App suggestions added to empty space</string> <string name="hotseat_tip_gaps_filled">App suggestions added to empty space</string>
<!-- tip shown when user migrates and predictions are enabled in hotseat --> <!-- tip shown when user migrates and predictions are enabled in hotseat -->
<string name="hotsaet_tip_prediction_enabled">App suggestions Enabled</string> <string name="hotsaet_tip_prediction_enabled">App suggestions enabled</string>
<!-- tip shown when hotseat edu is requested while predicions are disabled -->
<string name="hotsaet_tip_prediction_disabled">App suggestions are disabled</string>
<!-- content description for hotseat items --> <!-- content description for hotseat items -->
<string name="hotseat_prediction_content_description">Predicted app: <xliff:g id="title" example="Chrome">%1$s</xliff:g></string> <string name="hotseat_prediction_content_description">Predicted app: <xliff:g id="title" example="Chrome">%1$s</xliff:g></string>

View File

@ -31,6 +31,7 @@ import android.os.Bundle;
import android.os.CancellationSignal; import android.os.CancellationSignal;
import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.hybridhotseat.HotseatPredictionController;
import com.android.launcher3.model.WellbeingModel; import com.android.launcher3.model.WellbeingModel;
import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.proxy.ProxyActivityStarter; import com.android.launcher3.proxy.ProxyActivityStarter;
@ -75,6 +76,7 @@ public abstract class BaseQuickstepLauncher extends Launcher
private final ShelfPeekAnim mShelfPeekAnim = new ShelfPeekAnim(this); private final ShelfPeekAnim mShelfPeekAnim = new ShelfPeekAnim(this);
private OverviewActionsView mActionsView; private OverviewActionsView mActionsView;
protected HotseatPredictionController mHotseatPredictionController;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -305,6 +307,13 @@ public abstract class BaseQuickstepLauncher extends Launcher
return mShelfPeekAnim; return mShelfPeekAnim;
} }
/**
* Returns Prediction controller for hybrid hotseat
*/
public HotseatPredictionController getHotseatPredictionController() {
return mHotseatPredictionController;
}
public void setHintUserWillBeActive() { public void setHintUserWillBeActive() {
addActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE); addActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE);
} }

View File

@ -31,6 +31,7 @@ import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherState;
import com.android.launcher3.Workspace; import com.android.launcher3.Workspace;
import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.hybridhotseat.HotseatPredictionController;
import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.statemanager.StateManager.StateListener; import com.android.launcher3.statemanager.StateManager.StateListener;
import com.android.launcher3.util.OnboardingPrefs; import com.android.launcher3.util.OnboardingPrefs;
@ -100,6 +101,28 @@ public class QuickstepOnboardingPrefs extends OnboardingPrefs<BaseQuickstepLaunc
}); });
} }
if (!hasReachedMaxCount(HOTSEAT_DISCOVERY_TIP_COUNT)) {
stateManager.addStateListener(new StateListener<LauncherState>() {
boolean mFromAllApps = false;
@Override
public void onStateTransitionStart(LauncherState toState) {
mFromAllApps = mLauncher.getStateManager().getCurrentStableState() == ALL_APPS;
}
@Override
public void onStateTransitionComplete(LauncherState finalState) {
HotseatPredictionController client = mLauncher.getHotseatPredictionController();
if (mFromAllApps && finalState == NORMAL && client.hasPredictions()) {
if (incrementEventCount(HOTSEAT_DISCOVERY_TIP_COUNT)) {
client.showDiscoveryTip();
stateManager.removeStateListener(this);
}
}
}
});
}
if (SysUINavigationMode.getMode(launcher) == NO_BUTTON if (SysUINavigationMode.getMode(launcher) == NO_BUTTON
&& FeatureFlags.ENABLE_ALL_APPS_EDU.get()) { && FeatureFlags.ENABLE_ALL_APPS_EDU.get()) {
stateManager.addStateListener(new StateListener<LauncherState>() { stateManager.addStateListener(new StateListener<LauncherState>() {

View File

@ -34,6 +34,8 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:gravity="center" android:gravity="center"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:textColor="@android:color/white" android:textColor="@android:color/white"

View File

@ -37,13 +37,14 @@ public class OnboardingPrefs<T extends Launcher> {
public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count"; public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count";
public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count"; public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count";
public static final String ALL_APPS_COUNT = "launcher.all_apps_count"; public static final String ALL_APPS_COUNT = "launcher.all_apps_count";
public static final String HOTSEAT_DISCOVERY_TIP_COUNT = "launcher.hotseat_discovery_tip_count";
/** /**
* Events that either have happened or have not (booleans). * Events that either have happened or have not (booleans).
*/ */
@StringDef(value = { @StringDef(value = {
HOME_BOUNCE_SEEN, HOME_BOUNCE_SEEN,
SHELF_BOUNCE_SEEN, SHELF_BOUNCE_SEEN
}) })
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface EventBoolKey {} public @interface EventBoolKey {}
@ -55,6 +56,7 @@ public class OnboardingPrefs<T extends Launcher> {
HOME_BOUNCE_COUNT, HOME_BOUNCE_COUNT,
SHELF_BOUNCE_COUNT, SHELF_BOUNCE_COUNT,
ALL_APPS_COUNT, ALL_APPS_COUNT,
HOTSEAT_DISCOVERY_TIP_COUNT
}) })
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface EventCountKey {} public @interface EventCountKey {}
@ -65,6 +67,7 @@ public class OnboardingPrefs<T extends Launcher> {
maxCounts.put(HOME_BOUNCE_COUNT, 3); maxCounts.put(HOME_BOUNCE_COUNT, 3);
maxCounts.put(SHELF_BOUNCE_COUNT, 3); maxCounts.put(SHELF_BOUNCE_COUNT, 3);
maxCounts.put(ALL_APPS_COUNT, 5); maxCounts.put(ALL_APPS_COUNT, 5);
maxCounts.put(HOTSEAT_DISCOVERY_TIP_COUNT, 5);
MAX_COUNTS = Collections.unmodifiableMap(maxCounts); MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
} }