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
This commit is contained in:
Tony Wickham
2018-01-16 12:14:06 -08:00
parent a17274fb05
commit fb63fe85f2
8 changed files with 299 additions and 3 deletions

View File

@@ -0,0 +1,24 @@
<!--
~ 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
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/>
</vector>

View File

@@ -0,0 +1,39 @@
<?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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="48dp"
android:layout_width="match_parent"
android:background="@android:color/black"
android:layout_gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/recents_swipe_up_onboarding"
android:textColor="@android:color/white"
android:drawableBottom="@drawable/ic_chevron_up"/>
<ImageView
android:id="@+id/dismiss"
android:layout_width="48dp"
android:layout_height="48dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:paddingEnd="18dp"
android:src="@drawable/ic_close_white"
android:layout_gravity="center_vertical|end"/>
</FrameLayout>

View File

@@ -809,6 +809,8 @@
<string name="recents_stack_action_button_label">Clear all</string>
<!-- Recents: Hint text that shows on the drop targets to start multiwindow. [CHAR LIMIT=NONE] -->
<string name="recents_drag_hint_message">Drag here to use split screen</string>
<!-- Recents: Text that shows above the nav bar after launching a few apps. [CHAR LIMIT=NONE] -->
<string name="recents_swipe_up_onboarding">Swipe up to switch apps</string>
<!-- Recents: MultiStack add stack split horizontal radio button. [CHAR LIMIT=NONE] -->
<string name="recents_multistack_add_stack_dialog_split_horizontal">Split Horizontal</string>

View File

@@ -30,6 +30,7 @@ import android.app.ActivityManager.RecentTaskInfo;
import android.app.ActivityOptions;
import android.app.AppGlobals;
import android.app.IAssistDataReceiver;
import android.app.WindowConfiguration.ActivityType;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
@@ -96,11 +97,14 @@ public class ActivityManagerWrapper {
* @return the top running task (can be {@code null}).
*/
public ActivityManager.RunningTaskInfo getRunningTask() {
return getRunningTask(ACTIVITY_TYPE_RECENTS /* ignoreActivityType */);
}
public ActivityManager.RunningTaskInfo getRunningTask(@ActivityType int ignoreActivityType) {
// Note: The set of running tasks from the system is ordered by recency
try {
List<ActivityManager.RunningTaskInfo> tasks =
ActivityManager.getService().getFilteredTasks(1,
ACTIVITY_TYPE_RECENTS /* ignoreActivityType */,
ActivityManager.getService().getFilteredTasks(1, ignoreActivityType,
WINDOWING_MODE_PINNED /* ignoreWindowingMode */);
if (tasks.isEmpty()) {
return null;

View File

@@ -195,6 +195,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
return mOverviewProxy;
}
public ComponentName getLauncherComponent() {
return mLauncherComponentName;
}
private void disconnectFromLauncherService() {
if (mOverviewProxy != null) {
mOverviewProxy.asBinder().unlinkToDeath(mOverviewServiceDeathRcpt, 0);

View File

@@ -48,6 +48,8 @@ public final class Prefs {
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 @@ public final class Prefs {
@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) {

View File

@@ -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;
}
}

View File

@@ -57,10 +57,11 @@ import com.android.systemui.plugins.PluginListener;
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 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
private NavigationBarInflaterView mNavigationInflaterView;
private RecentsComponent mRecentsComponent;
private Divider mDivider;
private SwipeUpOnboarding mSwipeUpOnboarding;
private class NavTransitionListener implements TransitionListener {
private boolean mBackTransitioning;
@@ -206,6 +208,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
private final OverviewProxyListener mOverviewProxyListener = isConnected -> {
setSlippery(!isConnected);
setDisabledFlags(mDisabledFlags, true);
setUpSwipeUpOnboarding(isConnected);
};
public NavigationBarView(Context context, AttributeSet attrs) {
@@ -237,6 +240,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
new ButtonDispatcher(R.id.rotate_suggestion));
mOverviewProxyService = Dependency.get(OverviewProxyService.class);
mSwipeUpOnboarding = new SwipeUpOnboarding(context);
}
public BarTransitions getBarTransitions() {
@@ -740,6 +744,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
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 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
Dependency.get(PluginManager.class).addPluginListener(this,
NavGesture.class, false /* Only one */);
mOverviewProxyService.addCallback(mOverviewProxyListener);
setUpSwipeUpOnboarding(mOverviewProxyService.getProxy() != null);
}
@Override
@@ -839,6 +845,15 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
mGestureHelper.destroy();
}
mOverviewProxyService.removeCallback(mOverviewProxyListener);
setUpSwipeUpOnboarding(false);
}
private void setUpSwipeUpOnboarding(boolean connectedToOverviewProxy) {
if (connectedToOverviewProxy) {
mSwipeUpOnboarding.onConnectedToLauncher(mOverviewProxyService.getLauncherComponent());
} else {
mSwipeUpOnboarding.onDisconnectedFromLauncher();
}
}
@Override