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:
24
packages/SystemUI/res/drawable/ic_chevron_up.xml
Normal file
24
packages/SystemUI/res/drawable/ic_chevron_up.xml
Normal 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>
|
||||
39
packages/SystemUI/res/layout/recents_swipe_up_onboarding.xml
Normal file
39
packages/SystemUI/res/layout/recents_swipe_up_onboarding.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user