Add swipe up onboarding from apps
After launching 3 apps, we create a window at the bottom
attached to the nav bar to teach users to swipe for recents.
There is an X on this window to dismiss it, but we will keep
showing the onboarding every time they open apps until they
perform the swipe up action.
Test: manual
Bug: 70180942
Change-Id: I4b15fac918b7b1633a3c09ab0819f2acb1dce697
diff --git a/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java
index 0be522b..244c1b9 100644
--- a/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java
@@ -195,6 +195,10 @@
return mOverviewProxy;
}
+ public ComponentName getLauncherComponent() {
+ return mLauncherComponentName;
+ }
+
private void disconnectFromLauncherService() {
if (mOverviewProxy != null) {
mOverviewProxy.asBinder().unlinkToDeath(mOverviewServiceDeathRcpt, 0);
diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java
index 4437d31..9319bc6 100644
--- a/packages/SystemUI/src/com/android/systemui/Prefs.java
+++ b/packages/SystemUI/src/com/android/systemui/Prefs.java
@@ -48,6 +48,8 @@
Key.QS_WORK_ADDED,
Key.QS_NIGHTDISPLAY_ADDED,
Key.SEEN_MULTI_USER,
+ Key.NUM_APPS_LAUNCHED,
+ Key.HAS_SWIPED_UP_FOR_RECENTS,
})
public @interface Key {
@Deprecated
@@ -75,6 +77,8 @@
@Deprecated
String QS_NIGHTDISPLAY_ADDED = "QsNightDisplayAdded";
String SEEN_MULTI_USER = "HasSeenMultiUser";
+ String NUM_APPS_LAUNCHED = "NumAppsLaunched";
+ String HAS_SWIPED_UP_FOR_RECENTS = "HasSwipedUpForRecents";
}
public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/SwipeUpOnboarding.java b/packages/SystemUI/src/com/android/systemui/recents/SwipeUpOnboarding.java
new file mode 100644
index 0000000..f065f48
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/SwipeUpOnboarding.java
@@ -0,0 +1,204 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.recents;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.os.Build;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+
+import com.android.systemui.Prefs;
+import com.android.systemui.R;
+import com.android.systemui.recents.misc.SysUiTaskStackChangeListener;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+
+/**
+ * Shows onboarding for the new recents interaction in P (codenamed quickstep).
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class SwipeUpOnboarding {
+
+ private static final String TAG = "SwipeUpOnboarding";
+ private static final boolean RESET_PREFS_FOR_DEBUG = false;
+ private static final long SHOW_DELAY_MS = 500;
+ private static final long SHOW_HIDE_DURATION_MS = 300;
+ // Don't show the onboarding until the user has launched this number of apps.
+ private static final int SHOW_ON_APP_LAUNCH = 3;
+
+ private final Context mContext;
+ private final WindowManager mWindowManager;
+ private final View mLayout;
+
+ private boolean mTaskListenerRegistered;
+ private ComponentName mLauncherComponent;
+ private boolean mLayoutAttachedToWindow;
+
+ private final SysUiTaskStackChangeListener mTaskListener = new SysUiTaskStackChangeListener() {
+ @Override
+ public void onTaskStackChanged() {
+ ActivityManager.RunningTaskInfo info = ActivityManagerWrapper.getInstance()
+ .getRunningTask(ACTIVITY_TYPE_UNDEFINED /* ignoreActivityType */);
+ int activityType = info.configuration.windowConfiguration.getActivityType();
+ int numAppsLaunched = Prefs.getInt(mContext, Prefs.Key.NUM_APPS_LAUNCHED, 0);
+ if (activityType == ACTIVITY_TYPE_STANDARD) {
+ numAppsLaunched++;
+ if (numAppsLaunched >= SHOW_ON_APP_LAUNCH) {
+ show();
+ } else {
+ Prefs.putInt(mContext, Prefs.Key.NUM_APPS_LAUNCHED, numAppsLaunched);
+ }
+ } else {
+ String runningPackage = info.topActivity.getPackageName();
+ // TODO: use callback from the overview proxy service to handle this case
+ if (runningPackage.equals(mLauncherComponent.getPackageName())
+ && activityType == ACTIVITY_TYPE_RECENTS) {
+ Prefs.putBoolean(mContext, Prefs.Key.HAS_SWIPED_UP_FOR_RECENTS, true);
+ onDisconnectedFromLauncher();
+ } else {
+ hide(false);
+ }
+ }
+ }
+ };
+
+ private final View.OnAttachStateChangeListener mOnAttachStateChangeListener
+ = new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View view) {
+ if (view == mLayout) {
+ mLayoutAttachedToWindow = true;
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View view) {
+ if (view == mLayout) {
+ mLayoutAttachedToWindow = false;
+ }
+ }
+ };
+
+ public SwipeUpOnboarding(Context context) {
+ mContext = context;
+ mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ mLayout = LayoutInflater.from(mContext).inflate(R.layout.recents_swipe_up_onboarding, null);
+ mLayout.addOnAttachStateChangeListener(mOnAttachStateChangeListener);
+ mLayout.findViewById(R.id.dismiss).setOnClickListener(v -> hide(true));
+
+ if (RESET_PREFS_FOR_DEBUG) {
+ Prefs.putBoolean(mContext, Prefs.Key.HAS_SWIPED_UP_FOR_RECENTS, false);
+ Prefs.putInt(mContext, Prefs.Key.NUM_APPS_LAUNCHED, 0);
+ }
+ }
+
+ public void onConnectedToLauncher(ComponentName launcherComponent) {
+ mLauncherComponent = launcherComponent;
+ boolean alreadyLearnedSwipeUpForRecents = Prefs.getBoolean(mContext,
+ Prefs.Key.HAS_SWIPED_UP_FOR_RECENTS, false);
+ if (!mTaskListenerRegistered && !alreadyLearnedSwipeUpForRecents) {
+ ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskListener);
+ mTaskListenerRegistered = true;
+ }
+ }
+
+ public void onDisconnectedFromLauncher() {
+ if (mTaskListenerRegistered) {
+ ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskListener);
+ mTaskListenerRegistered = false;
+ }
+ hide(false);
+ }
+
+ public void onConfigurationChanged(Configuration newConfiguration) {
+ if (newConfiguration.orientation != Configuration.ORIENTATION_PORTRAIT) {
+ hide(false);
+ }
+ }
+
+ public void show() {
+ // Only show in portrait.
+ int orientation = mContext.getResources().getConfiguration().orientation;
+ if (!mLayoutAttachedToWindow && orientation == Configuration.ORIENTATION_PORTRAIT) {
+ mLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ mWindowManager.addView(mLayout, getWindowLayoutParams());
+ int layoutHeight = mLayout.getHeight();
+ if (layoutHeight == 0) {
+ mLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ layoutHeight = mLayout.getMeasuredHeight();
+ }
+ mLayout.setTranslationY(layoutHeight);
+ mLayout.setAlpha(0);
+ mLayout.animate()
+ .translationY(0)
+ .alpha(1f)
+ .withLayer()
+ .setStartDelay(SHOW_DELAY_MS)
+ .setDuration(SHOW_HIDE_DURATION_MS)
+ .setInterpolator(new DecelerateInterpolator())
+ .start();
+ }
+ }
+
+ public void hide(boolean animate) {
+ if (mLayoutAttachedToWindow) {
+ if (animate) {
+ mLayout.animate()
+ .translationY(mLayout.getHeight())
+ .alpha(0f)
+ .withLayer()
+ .setDuration(SHOW_HIDE_DURATION_MS)
+ .setInterpolator(new AccelerateInterpolator())
+ .withEndAction(() -> mWindowManager.removeView(mLayout))
+ .start();
+ } else {
+ mWindowManager.removeView(mLayout);
+ }
+ }
+ }
+
+ private WindowManager.LayoutParams getWindowLayoutParams() {
+ int flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG,
+ flags,
+ PixelFormat.TRANSLUCENT);
+ lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+ lp.setTitle("SwipeUpOnboarding");
+ lp.gravity = Gravity.BOTTOM;
+ return lp;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 059ce92..4b20a89 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -57,10 +57,11 @@
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.plugins.statusbar.phone.NavGesture;
import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
+import com.android.systemui.recents.SwipeUpOnboarding;
import com.android.systemui.stackdivider.Divider;
-import com.android.systemui.statusbar.policy.TintedKeyButtonDrawable;
import com.android.systemui.statusbar.policy.DeadZone;
import com.android.systemui.statusbar.policy.KeyButtonDrawable;
+import com.android.systemui.statusbar.policy.TintedKeyButtonDrawable;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -124,6 +125,7 @@
private NavigationBarInflaterView mNavigationInflaterView;
private RecentsComponent mRecentsComponent;
private Divider mDivider;
+ private SwipeUpOnboarding mSwipeUpOnboarding;
private class NavTransitionListener implements TransitionListener {
private boolean mBackTransitioning;
@@ -206,6 +208,7 @@
private final OverviewProxyListener mOverviewProxyListener = isConnected -> {
setSlippery(!isConnected);
setDisabledFlags(mDisabledFlags, true);
+ setUpSwipeUpOnboarding(isConnected);
};
public NavigationBarView(Context context, AttributeSet attrs) {
@@ -237,6 +240,7 @@
new ButtonDispatcher(R.id.rotate_suggestion));
mOverviewProxyService = Dependency.get(OverviewProxyService.class);
+ mSwipeUpOnboarding = new SwipeUpOnboarding(context);
}
public BarTransitions getBarTransitions() {
@@ -740,6 +744,7 @@
updateTaskSwitchHelper();
updateIcons(getContext(), mConfiguration, newConfig);
updateRecentsIcon();
+ mSwipeUpOnboarding.onConfigurationChanged(newConfig);
if (uiCarModeChanged || mConfiguration.densityDpi != newConfig.densityDpi
|| mConfiguration.getLayoutDirection() != newConfig.getLayoutDirection()) {
// If car mode or density changes, we need to reset the icons.
@@ -829,6 +834,7 @@
Dependency.get(PluginManager.class).addPluginListener(this,
NavGesture.class, false /* Only one */);
mOverviewProxyService.addCallback(mOverviewProxyListener);
+ setUpSwipeUpOnboarding(mOverviewProxyService.getProxy() != null);
}
@Override
@@ -839,6 +845,15 @@
mGestureHelper.destroy();
}
mOverviewProxyService.removeCallback(mOverviewProxyListener);
+ setUpSwipeUpOnboarding(false);
+ }
+
+ private void setUpSwipeUpOnboarding(boolean connectedToOverviewProxy) {
+ if (connectedToOverviewProxy) {
+ mSwipeUpOnboarding.onConnectedToLauncher(mOverviewProxyService.getLauncherComponent());
+ } else {
+ mSwipeUpOnboarding.onDisconnectedFromLauncher();
+ }
}
@Override