diff options
8 files changed, 299 insertions, 3 deletions
| diff --git a/packages/SystemUI/res/drawable/ic_chevron_up.xml b/packages/SystemUI/res/drawable/ic_chevron_up.xml new file mode 100644 index 000000000000..835d0adbef58 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_chevron_up.xml @@ -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> diff --git a/packages/SystemUI/res/layout/recents_swipe_up_onboarding.xml b/packages/SystemUI/res/layout/recents_swipe_up_onboarding.xml new file mode 100644 index 000000000000..b6bd3b32995e --- /dev/null +++ b/packages/SystemUI/res/layout/recents_swipe_up_onboarding.xml @@ -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>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 6437903bd12a..0b2f2d27ec31 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -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> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index 1c99d3846d8e..c9a6ea9939f5 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -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; diff --git a/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java index 0be522bfb8cb..244c1b990448 100644 --- a/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/OverviewProxyService.java @@ -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); diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java index 4437d314a7c2..9319bc60f9ef 100644 --- a/packages/SystemUI/src/com/android/systemui/Prefs.java +++ b/packages/SystemUI/src/com/android/systemui/Prefs.java @@ -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) { 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 000000000000..f065f48b20b5 --- /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 059ce929b290..4b20a896982a 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.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 |