diff --git a/.gitignore b/.gitignore index 7240e4877b..694b40c7ef 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ bin/ local.properties gradle/ build/ -gradlew* \ No newline at end of file +gradlew* +.DS_Store diff --git a/Android.mk b/Android.mk index fbe19b0b63..5614e25cac 100644 --- a/Android.mk +++ b/Android.mk @@ -101,6 +101,7 @@ LOCAL_MODULE_TAGS := optional LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib LOCAL_SRC_FILES := \ $(call all-java-files-under, src) \ + $(call all-java-files-under, src_shortcuts_overrides) \ $(call all-java-files-under, src_ui_overrides) \ $(call all-java-files-under, src_flags) @@ -131,7 +132,7 @@ LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib LOCAL_SRC_FILES := \ $(call all-java-files-under, src) \ $(call all-java-files-under, src_ui_overrides) \ - $(call all-java-files-under, go/src_flags) + $(call all-java-files-under, go/src) LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/go/res @@ -174,7 +175,8 @@ LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib LOCAL_SRC_FILES := \ $(call all-java-files-under, src) \ $(call all-java-files-under, quickstep/src) \ - $(call all-java-files-under, src_flags) + $(call all-java-files-under, src_flags) \ + $(call all-java-files-under, src_shortcuts_overrides) LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/quickstep/res LOCAL_PROGUARD_ENABLED := disabled @@ -235,7 +237,7 @@ LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib LOCAL_SRC_FILES := \ $(call all-java-files-under, src) \ $(call all-java-files-under, quickstep/src) \ - $(call all-java-files-under, go/src_flags) + $(call all-java-files-under, go/src) LOCAL_RESOURCE_DIR := \ $(LOCAL_PATH)/quickstep/res \ diff --git a/build.gradle b/build.gradle index 1b9df53d6a..476e92bf67 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ android { sourceSets { main { res.srcDirs = ['res'] - java.srcDirs = ['src'] + java.srcDirs = ['src', 'src_shortcuts_overrides'] manifest.srcFile 'AndroidManifest-common.xml' proto { srcDir 'protos/' @@ -100,7 +100,7 @@ android { l3go { res.srcDirs = ['go/res'] - java.srcDirs = ['go/src_flags', "src_ui_overrides"] + java.srcDirs = ['go/src', "src_ui_overrides"] manifest.srcFile "go/AndroidManifest.xml" } diff --git a/go/src_flags/com/android/launcher3/config/FeatureFlags.java b/go/src/com/android/launcher3/config/FeatureFlags.java similarity index 100% rename from go/src_flags/com/android/launcher3/config/FeatureFlags.java rename to go/src/com/android/launcher3/config/FeatureFlags.java diff --git a/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java new file mode 100644 index 0000000000..ff0c907f55 --- /dev/null +++ b/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java @@ -0,0 +1,124 @@ +/* + * 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.launcher3.shortcuts; + +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; + +import com.android.launcher3.ItemInfo; + +import java.util.Collections; +import java.util.List; + +/** + * Performs operations related to deep shortcuts, such as querying for them, pinning them, etc. + */ +public class DeepShortcutManager { + private static DeepShortcutManager sInstance; + private static final Object sInstanceLock = new Object(); + + public static DeepShortcutManager getInstance(Context context) { + synchronized (sInstanceLock) { + if (sInstance == null) { + sInstance = new DeepShortcutManager(context.getApplicationContext()); + } + return sInstance; + } + } + + private DeepShortcutManager(Context context) { + } + + public static boolean supportsShortcuts(ItemInfo info) { + return false; + } + + public boolean wasLastCallSuccess() { + return false; + } + + public void onShortcutsChanged(List shortcuts) { + } + + /** + * Queries for the shortcuts with the package name and provided ids. + * + * This method is intended to get the full details for shortcuts when they are added or updated, + * because we only get "key" fields in onShortcutsChanged(). + */ + public List queryForFullDetails(String packageName, + List shortcutIds, UserHandle user) { + return Collections.emptyList(); + } + + /** + * Gets all the manifest and dynamic shortcuts associated with the given package and user, + * to be displayed in the shortcuts container on long press. + */ + public List queryForShortcutsContainer(ComponentName activity, + UserHandle user) { + return Collections.emptyList(); + } + + /** + * Removes the given shortcut from the current list of pinned shortcuts. + * (Runs on background thread) + */ + public void unpinShortcut(final ShortcutKey key) { + } + + /** + * Adds the given shortcut to the current list of pinned shortcuts. + * (Runs on background thread) + */ + public void pinShortcut(final ShortcutKey key) { + } + + public void startShortcut(String packageName, String id, Rect sourceBounds, + Bundle startActivityOptions, UserHandle user) { + } + + public Drawable getShortcutIconDrawable(ShortcutInfoCompat shortcutInfo, int density) { + return null; + } + + /** + * Returns the id's of pinned shortcuts associated with the given package and user. + * + * If packageName is null, returns all pinned shortcuts regardless of package. + */ + public List queryForPinnedShortcuts(String packageName, UserHandle user) { + return Collections.emptyList(); + } + + public List queryForPinnedShortcuts(String packageName, + List shortcutIds, UserHandle user) { + return Collections.emptyList(); + } + + public List queryForAllShortcuts(UserHandle user) { + return Collections.emptyList(); + } + + public boolean hasHostPermission() { + return false; + } +} diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java index 5680a67090..d7bbfe06e1 100644 --- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java +++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java @@ -178,6 +178,14 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag @Override public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) { if (hasControlRemoteAppTransitionPermission()) { + boolean fromRecents = mLauncher.getStateManager().getState().overviewUi + && findTaskViewToLaunch(launcher, v, null) != null; + RecentsView recentsView = mLauncher.getOverviewPanel(); + if (fromRecents && recentsView.getQuickScrubController().isQuickSwitch()) { + return ActivityOptions.makeCustomAnimation(mLauncher, R.anim.no_anim, + R.anim.no_anim); + } + RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler, true /* startAtFrontOfQueue */) { @@ -218,8 +226,6 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag } }; - boolean fromRecents = mLauncher.getStateManager().getState().overviewUi - && findTaskViewToLaunch(launcher, v, null) != null; int duration = fromRecents ? RECENTS_LAUNCH_DURATION : APP_LAUNCH_DURATION; diff --git a/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java index 26453022f0..1d65a54e7a 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java @@ -35,6 +35,7 @@ public class FastOverviewState extends OverviewState { * Vertical transition of the task previews relative to the full container. */ public static final float OVERVIEW_TRANSLATION_FACTOR = 0.4f; + public static final float OVERVIEW_CENTERED_TRANSLATION_FACTOR = 0.5f; private static final int STATE_FLAGS = FLAG_DISABLE_RESTORE | FLAG_DISABLE_INTERACTION | FLAG_OVERVIEW_UI | FLAG_HIDE_BACK_BUTTON | FLAG_DISABLE_ACCESSIBILITY; @@ -60,12 +61,17 @@ public class FastOverviewState extends OverviewState { RecentsView recentsView = launcher.getOverviewPanel(); recentsView.getTaskSize(sTempRect); - return new float[] {getOverviewScale(launcher.getDeviceProfile(), sTempRect, launcher), - OVERVIEW_TRANSLATION_FACTOR}; + boolean isQuickSwitch = recentsView.getQuickScrubController().isQuickSwitch(); + float translationYFactor = isQuickSwitch + ? OVERVIEW_CENTERED_TRANSLATION_FACTOR + : OVERVIEW_TRANSLATION_FACTOR; + return new float[] {getOverviewScale(launcher.getDeviceProfile(), sTempRect, launcher, + isQuickSwitch), translationYFactor}; } - public static float getOverviewScale(DeviceProfile dp, Rect taskRect, Context context) { - if (dp.isVerticalBarLayout()) { + public static float getOverviewScale(DeviceProfile dp, Rect taskRect, Context context, + boolean isQuickSwitch) { + if (dp.isVerticalBarLayout() && !isQuickSwitch) { return 1f; } @@ -73,6 +79,10 @@ public class FastOverviewState extends OverviewState { float usedHeight = taskRect.height() + res.getDimension(R.dimen.task_thumbnail_top_margin); float usedWidth = taskRect.width() + 2 * (res.getDimension(R.dimen.recents_page_spacing) + res.getDimension(R.dimen.quickscrub_adjacent_visible_width)); + if (isQuickSwitch) { + usedWidth = taskRect.width(); + return Math.max(dp.availableHeightPx / usedHeight, dp.availableWidthPx / usedWidth); + } return Math.min(Math.min(dp.availableHeightPx / usedHeight, dp.availableWidthPx / usedWidth), MAX_PREVIEW_SCALE_UP); } diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java index c809e28320..85eed1fbfe 100644 --- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java +++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java @@ -16,7 +16,6 @@ package com.android.quickstep; import static android.view.View.TRANSLATION_Y; - import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.LauncherState.BACKGROUND_APP; @@ -58,6 +57,7 @@ import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.launcher3.allapps.DiscoveryBounce; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.compat.AccessibilityManagerCompat; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.uioverrides.FastOverviewState; import com.android.launcher3.userevent.nano.LauncherLogProto; @@ -192,7 +192,8 @@ public interface ActivityControlHelper { @InteractionType int interactionType, TransformedRect outRect) { LayoutUtils.calculateLauncherTaskSize(context, dp, outRect.rect); if (interactionType == INTERACTION_QUICK_SCRUB) { - outRect.scale = FastOverviewState.getOverviewScale(dp, outRect.rect, context); + outRect.scale = FastOverviewState.getOverviewScale(dp, outRect.rect, context, + FeatureFlags.QUICK_SWITCH.get()); } if (dp.isVerticalBarLayout()) { Rect targetInsets = dp.getInsets(); diff --git a/quickstep/src/com/android/quickstep/QuickScrubController.java b/quickstep/src/com/android/quickstep/QuickScrubController.java index 34207670e8..c44ccd3ded 100644 --- a/quickstep/src/com/android/quickstep/QuickScrubController.java +++ b/quickstep/src/com/android/quickstep/QuickScrubController.java @@ -16,8 +16,18 @@ package com.android.quickstep; +import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; +import static com.android.launcher3.anim.Interpolators.ACCEL; +import static com.android.launcher3.anim.Interpolators.DEACCEL_3; import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.os.Build; +import android.util.FloatProperty; import android.util.Log; import android.view.HapticFeedbackConstants; import android.view.animation.Interpolator; @@ -37,8 +47,10 @@ import com.android.quickstep.views.TaskView; * The behavior is to evenly divide the progress into sections, each of which scrolls one page. * The first and last section set an alarm to auto-advance backwards or forwards, respectively. */ +@TargetApi(Build.VERSION_CODES.P) public class QuickScrubController implements OnAlarmListener { + public static final int QUICK_SWITCH_FROM_APP_START_DURATION = 0; public static final int QUICK_SCRUB_FROM_APP_START_DURATION = 240; public static final int QUICK_SCRUB_FROM_HOME_START_DURATION = 200; // We want the translation y to finish faster than the rest of the animation. @@ -52,6 +64,19 @@ public class QuickScrubController implements OnAlarmListener { 0.05f, 0.20f, 0.35f, 0.50f, 0.65f, 0.80f, 0.95f }; + private static final FloatProperty PROGRESS + = new FloatProperty("progress") { + @Override + public void setValue(QuickScrubController quickScrubController, float progress) { + quickScrubController.onQuickScrubProgress(progress); + } + + @Override + public Float get(QuickScrubController quickScrubController) { + return quickScrubController.mEndProgress; + } + }; + private static final String TAG = "QuickScrubController"; private static final boolean ENABLE_AUTO_ADVANCE = true; private static final long AUTO_ADVANCE_DELAY = 500; @@ -72,6 +97,13 @@ public class QuickScrubController implements OnAlarmListener { private ActivityControlHelper mActivityControlHelper; private TouchInteractionLog mTouchInteractionLog; + private boolean mIsQuickSwitch; + private float mStartProgress; + private float mEndProgress; + private float mPrevProgressDelta; + private float mPrevPrevProgressDelta; + private boolean mShouldSwitchToNext; + public QuickScrubController(BaseActivity activity, RecentsView recentsView) { mActivity = activity; mRecentsView = recentsView; @@ -91,17 +123,26 @@ public class QuickScrubController implements OnAlarmListener { mActivityControlHelper = controlHelper; mTouchInteractionLog = touchInteractionLog; + if (mIsQuickSwitch) { + mShouldSwitchToNext = true; + mPrevProgressDelta = 0; + if (mRecentsView.getTaskViewCount() > 0) { + mRecentsView.getTaskViewAt(0).setFullscreen(true); + } + if (mRecentsView.getTaskViewCount() > 1) { + mRecentsView.getTaskViewAt(1).setFullscreen(true); + } + } + snapToNextTaskIfAvailable(); mActivity.getUserEventDispatcher().resetActionDurationMillis(); } public void onQuickScrubEnd() { mInQuickScrub = false; - if (ENABLE_AUTO_ADVANCE) { - mAutoAdvanceAlarm.cancelAlarm(); - } - int page = mRecentsView.getNextPage(); + Runnable launchTaskRunnable = () -> { + int page = mRecentsView.getPageNearestToCenterOfScreen(); TaskView taskView = mRecentsView.getTaskViewAt(page); if (taskView != null) { mWaitingForTaskLaunch = true; @@ -118,12 +159,49 @@ public class QuickScrubController implements OnAlarmListener { TaskUtils.getLaunchComponentKeyForTask(taskView.getTask().key)); } mWaitingForTaskLaunch = false; + if (mIsQuickSwitch) { + mIsQuickSwitch = false; + if (mRecentsView.getTaskViewCount() > 0) { + mRecentsView.getTaskViewAt(0).setFullscreen(false); + } + if (mRecentsView.getTaskViewCount() > 1) { + mRecentsView.getTaskViewAt(1).setFullscreen(false); + } + } + }, taskView.getHandler()); } else { breakOutOfQuickScrub(); } mActivityControlHelper = null; }; + + if (mIsQuickSwitch) { + float progressVelocity = mPrevPrevProgressDelta / SINGLE_FRAME_MS; + // Move to the next frame immediately, then start the animation from the + // following frame since it starts a frame later. + float singleFrameProgress = progressVelocity * SINGLE_FRAME_MS; + float fromProgress = mEndProgress + singleFrameProgress; + onQuickScrubProgress(fromProgress); + fromProgress += singleFrameProgress; + float toProgress = mShouldSwitchToNext ? 1 : 0; + int duration = (int) Math.abs((toProgress - fromProgress) / progressVelocity); + duration = Utilities.boundToRange(duration, 80, 300); + Animator anim = ObjectAnimator.ofFloat(this, PROGRESS, fromProgress, toProgress); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + launchTaskRunnable.run(); + } + }); + anim.setDuration(duration).start(); + return; + } + + if (ENABLE_AUTO_ADVANCE) { + mAutoAdvanceAlarm.cancelAlarm(); + } + int page = mRecentsView.getNextPage(); int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen()) * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE; if (mRecentsView.getChildCount() > 0 && mRecentsView.snapToPage(page, snapDuration)) { @@ -151,19 +229,28 @@ public class QuickScrubController implements OnAlarmListener { mLaunchingTaskId = 0; } + public boolean prepareQuickScrub(String tag) { + return prepareQuickScrub(tag, mIsQuickSwitch); + } + /** * Initializes the UI for quick scrub, returns true if success. */ - public boolean prepareQuickScrub(String tag) { + public boolean prepareQuickScrub(String tag, boolean isQuickSwitch) { if (mWaitingForTaskLaunch || mInQuickScrub) { Log.d(tag, "Waiting for last scrub to finish, will skip this interaction"); return false; } mOnFinishedTransitionToQuickScrubRunnable = null; mRecentsView.setNextPageSwitchRunnable(null); + mIsQuickSwitch = isQuickSwitch; return true; } + public boolean isQuickSwitch() { + return mIsQuickSwitch; + } + public boolean isWaitingForTaskLaunch() { return mWaitingForTaskLaunch; } @@ -179,6 +266,40 @@ public class QuickScrubController implements OnAlarmListener { } public void onQuickScrubProgress(float progress) { + if (mIsQuickSwitch) { + TaskView currentPage = mRecentsView.getTaskViewAt(0); + TaskView nextPage = mRecentsView.getTaskViewAt(1); + if (currentPage == null || nextPage == null) { + return; + } + if (!mFinishedTransitionToQuickScrub) { + mStartProgress = mEndProgress = progress; + } else { + float progressDelta = progress - mEndProgress; + mEndProgress = progress; + progress = Utilities.boundToRange(progress, mStartProgress, 1); + progress = Utilities.mapToRange(progress, mStartProgress, 1, 0, 1, LINEAR); + if (mInQuickScrub) { + mShouldSwitchToNext = mPrevProgressDelta > 0.007f || progressDelta > 0.007f + || progress >= 0.5f; + } + mPrevPrevProgressDelta = mPrevProgressDelta; + mPrevProgressDelta = progressDelta; + float scrollDiff = nextPage.getWidth() + mRecentsView.getPageSpacing(); + int scrollDir = mRecentsView.isRtl() ? -1 : 1; + int linearScrollDiff = (int) (progress * scrollDiff * scrollDir); + float accelScrollDiff = ACCEL.getInterpolation(progress) * scrollDiff * scrollDir; + currentPage.setZoomScale(1 - DEACCEL_3.getInterpolation(progress) + * TaskView.EDGE_SCALE_DOWN_FACTOR); + currentPage.setTranslationX(linearScrollDiff + accelScrollDiff); + nextPage.setTranslationZ(1); + nextPage.setTranslationY(currentPage.getTranslationY()); + int startScroll = mRecentsView.isRtl() ? mRecentsView.getMaxScrollX() : 0; + mRecentsView.setScrollX(startScroll + linearScrollDiff); + } + return; + } + int quickScrubSection = 0; for (float threshold : QUICK_SCRUB_THRESHOLDS) { if (progress < threshold) { @@ -228,9 +349,14 @@ public class QuickScrubController implements OnAlarmListener { public void snapToNextTaskIfAvailable() { if (mInQuickScrub && mRecentsView.getChildCount() > 0) { - int duration = mStartedFromHome ? QUICK_SCRUB_FROM_HOME_START_DURATION - : QUICK_SCRUB_FROM_APP_START_DURATION; - int pageToGoTo = mStartedFromHome ? 0 : mRecentsView.getNextPage() + 1; + int duration = mIsQuickSwitch + ? QUICK_SWITCH_FROM_APP_START_DURATION + : mStartedFromHome + ? QUICK_SCRUB_FROM_HOME_START_DURATION + : QUICK_SCRUB_FROM_APP_START_DURATION; + int pageToGoTo = mStartedFromHome || mIsQuickSwitch + ? 0 + : mRecentsView.getNextPage() + 1; goToPageWithHaptic(pageToGoTo, duration, true /* forceHaptic */, QUICK_SCRUB_START_INTERPOLATOR); } diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java index 9ea88842d7..a604da03e6 100644 --- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java +++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java @@ -23,6 +23,7 @@ import static com.android.launcher3.anim.Interpolators.DEACCEL; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2; import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_FROM_APP_START_DURATION; +import static com.android.quickstep.QuickScrubController.QUICK_SWITCH_FROM_APP_START_DURATION; import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD; @@ -59,6 +60,7 @@ import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logging.UserEventDispatcher; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; @@ -979,12 +981,14 @@ public class WindowTransformSwipeHandler { setStateOnUiThread(STATE_QUICK_SCRUB_START | STATE_GESTURE_COMPLETED); // Start the window animation without waiting for launcher. - animateToProgress(mCurrentShift.value, 1f, QUICK_SCRUB_FROM_APP_START_DURATION, LINEAR, - true /* goingToHome */); + long duration = FeatureFlags.QUICK_SWITCH.get() + ? QUICK_SWITCH_FROM_APP_START_DURATION + : QUICK_SCRUB_FROM_APP_START_DURATION; + animateToProgress(mCurrentShift.value, 1f, duration, LINEAR, true /* goingToHome */); } private void onQuickScrubStartUi() { - if (!mQuickScrubController.prepareQuickScrub(TAG)) { + if (!mQuickScrubController.prepareQuickScrub(TAG, FeatureFlags.QUICK_SWITCH.get())) { mQuickScrubBlocked = true; setStateOnUiThread(STATE_RESUME_LAST_TASK | STATE_HANDLER_INVALIDATED); return; @@ -993,6 +997,7 @@ public class WindowTransformSwipeHandler { mLauncherTransitionController.getAnimationPlayer().end(); mLauncherTransitionController = null; } + mLayoutListener.finish(); mActivityControlHelper.onQuickInteractionStart(mActivity, mRunningTaskInfo, false, mTouchInteractionLog); @@ -1008,6 +1013,13 @@ public class WindowTransformSwipeHandler { mQuickScrubController.onFinishedTransitionToQuickScrub(); mRecentsView.animateUpRunningTaskIconScale(); + if (mQuickScrubController.isQuickSwitch()) { + TaskView runningTask = mRecentsView.getRunningTaskView(); + if (runningTask != null) { + runningTask.setTranslationY(-mActivity.getResources().getDimension( + R.dimen.task_thumbnail_half_top_margin) * 1f / mRecentsView.getScaleX()); + } + } RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG); } diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java index ce65de1e09..c92c8d66b8 100644 --- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java +++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java @@ -17,7 +17,6 @@ package com.android.quickstep.views; import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN; - import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; @@ -33,7 +32,7 @@ import android.util.AttributeSet; import android.util.FloatProperty; import android.util.Property; import android.view.View; - +import android.view.ViewGroup; import com.android.launcher3.BaseActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; @@ -78,6 +77,7 @@ public class TaskThumbnailView extends View { private final Matrix mMatrix = new Matrix(); private float mClipBottom = -1; + private Rect mScaledInsets = new Rect(); private Task mTask; private ThumbnailData mThumbnailData; @@ -179,7 +179,17 @@ public class TaskThumbnailView extends View { @Override protected void onDraw(Canvas canvas) { - drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), mCornerRadius); + if (((TaskView) getParent()).isFullscreen()) { + // Draw the insets if we're being drawn fullscreen (we do this for quick switch). + drawOnCanvas(canvas, + -mScaledInsets.left, + -mScaledInsets.top, + getMeasuredWidth() + mScaledInsets.right, + getMeasuredHeight() + mScaledInsets.bottom, + mCornerRadius); + } else { + drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), mCornerRadius); + } } public float getCornerRadius() { @@ -253,6 +263,9 @@ public class TaskThumbnailView extends View { : getMeasuredWidth() / thumbnailWidth; } + mScaledInsets.set(thumbnailInsets); + Utilities.scaleRect(mScaledInsets, thumbnailScale); + if (rotate) { int rotationDir = profile.isVerticalBarLayout() && !profile.isSeascape() ? -1 : 1; mMatrix.setRotate(90 * rotationDir); diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index e48a05ab8f..31aaab0607 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -86,7 +86,7 @@ public class TaskView extends FrameLayout implements PageCallbacks { /** * How much to scale down pages near the edge of the screen. */ - private static final float EDGE_SCALE_DOWN_FACTOR = 0.03f; + public static final float EDGE_SCALE_DOWN_FACTOR = 0.03f; public static final long SCALE_ICON_DURATION = 120; private static final long DIM_ANIM_DURATION = 700; @@ -141,6 +141,7 @@ public class TaskView extends FrameLayout implements PageCallbacks { private IconView mIconView; private float mCurveScale; private float mZoomScale; + private boolean mIsFullscreen; private Animator mIconAndDimAnimator; private float mFocusTransitionProgress = 1; @@ -508,4 +509,18 @@ public class TaskView extends FrameLayout implements PageCallbacks { Log.w(tag, msg); Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show(); } + + /** + * Hides the icon and shows insets when this TaskView is about to be shown fullscreen. + */ + public void setFullscreen(boolean isFullscreen) { + mIsFullscreen = isFullscreen; + mIconView.setVisibility(mIsFullscreen ? INVISIBLE : VISIBLE); + setClipChildren(!mIsFullscreen); + setClipToPadding(!mIsFullscreen); + } + + public boolean isFullscreen() { + return mIsFullscreen; + } } diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk new file mode 100644 index 0000000000..501176475a --- /dev/null +++ b/robolectric_tests/Android.mk @@ -0,0 +1,53 @@ +# 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. + +############################################# +# Launcher Robolectric test target. # +############################################# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE := LauncherRoboTests +LOCAL_SDK_VERSION := current +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_STATIC_JAVA_LIBRARIES := \ + androidx.test.runner \ + androidx.test.rules \ + mockito-robolectric-prebuilt \ + truth-prebuilt +LOCAL_JAVA_LIBRARIES := \ + platform-robolectric-3.6.1-prebuilt + +LOCAL_INSTRUMENTATION_FOR := Launcher3 +LOCAL_MODULE_TAGS := optional + +include $(BUILD_STATIC_JAVA_LIBRARY) + +############################################ +# Target to run the previous target. # +############################################ +include $(CLEAR_VARS) + +LOCAL_MODULE := RunLauncherRoboTests +LOCAL_SDK_VERSION := current +LOCAL_JAVA_LIBRARIES := \ + LauncherRoboTests + +LOCAL_TEST_PACKAGE := Launcher3 + +LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src \ + +LOCAL_ROBOTEST_TIMEOUT := 36000 + +include prebuilts/misc/common/robolectric/3.6.1/run_robotests.mk diff --git a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java new file mode 100644 index 0000000000..faec380afd --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java @@ -0,0 +1,81 @@ +/* + * 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.launcher3.util; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Robolectric unit tests for {@link IntSet} + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 26) +public class IntSetTest { + + @Test + public void shouldBeEmptyInitially() { + IntSet set = new IntSet(); + assertThat(set.size()).isEqualTo(0); + } + + @Test + public void oneElementSet() { + IntSet set = new IntSet(); + set.add(2); + assertThat(set.size()).isEqualTo(1); + assertTrue(set.contains(2)); + assertFalse(set.contains(1)); + } + + + @Test + public void twoElementSet() { + IntSet set = new IntSet(); + set.add(2); + set.add(1); + assertThat(set.size()).isEqualTo(2); + assertTrue(set.contains(2)); + assertTrue(set.contains(1)); + } + + @Test + public void threeElementSet() { + IntSet set = new IntSet(); + set.add(2); + set.add(1); + set.add(10); + assertThat(set.size()).isEqualTo(3); + assertEquals("1, 2, 10", set.mArray.toConcatString()); + } + + + @Test + public void duplicateEntries() { + IntSet set = new IntSet(); + set.add(2); + set.add(2); + assertEquals(1, set.size()); + } +} diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 36b9e97aa4..9470635cb1 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -625,6 +625,10 @@ public abstract class PagedView extends ViewGrou mMaxScrollX = computeMaxScrollX(); } + public int getMaxScrollX() { + return mMaxScrollX; + } + protected int computeMaxScrollX() { int childCount = getChildCount(); if (childCount > 0) { @@ -640,6 +644,10 @@ public abstract class PagedView extends ViewGrou requestLayout(); } + public int getPageSpacing() { + return mPageSpacing; + } + private void dispatchPageCountChanged() { if (mPageIndicator != null) { mPageIndicator.setMarkersCount(getChildCount()); diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java index 64b5652c5c..e5a8a01f5d 100644 --- a/src/com/android/launcher3/config/BaseFlags.java +++ b/src/com/android/launcher3/config/BaseFlags.java @@ -22,9 +22,6 @@ import android.content.Context; import android.content.SharedPreferences; import android.provider.Settings; -import androidx.annotation.GuardedBy; -import androidx.annotation.Keep; - import com.android.launcher3.Utilities; import java.util.ArrayList; @@ -32,6 +29,9 @@ import java.util.List; import java.util.SortedMap; import java.util.TreeMap; +import androidx.annotation.GuardedBy; +import androidx.annotation.Keep; + /** * Defines a set of flags used to control various launcher behaviors. * @@ -87,6 +87,9 @@ abstract class BaseFlags { // trying to make them fit the orientation the device is in. public static final boolean OVERVIEW_USE_SCREENSHOT_ORIENTATION = true; + public static final TogglableFlag QUICK_SWITCH = new TogglableFlag("QUICK_SWITCH", false, + "Swiping right on the nav bar while in an app switches to the previous app"); + /** * Feature flag to handle define config changes dynamically instead of killing the process. */ diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java similarity index 100% rename from src/com/android/launcher3/shortcuts/DeepShortcutManager.java rename to src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml index 439058cbda..46b463b21d 100644 --- a/tests/AndroidManifest-common.xml +++ b/tests/AndroidManifest-common.xml @@ -18,6 +18,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.launcher3.tests"> + + diff --git a/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java b/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java index 04c04f5b11..0edb3d61bc 100644 --- a/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java +++ b/tests/src/com/android/launcher3/testcomponent/TestCommandReceiver.java @@ -19,6 +19,8 @@ import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; import static android.content.pm.PackageManager.DONT_KILL_APP; +import android.app.Activity; +import android.app.ActivityManager; import android.app.Instrumentation; import android.content.ComponentName; import android.content.ContentProvider; @@ -36,6 +38,7 @@ public class TestCommandReceiver extends ContentProvider { public static final String ENABLE_TEST_LAUNCHER = "enable-test-launcher"; public static final String DISABLE_TEST_LAUNCHER = "disable-test-launcher"; + public static final String KILL_PROCESS = "kill-process"; @Override public boolean onCreate() { @@ -83,14 +86,22 @@ public class TestCommandReceiver extends ContentProvider { COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP); return null; } - + case KILL_PROCESS: { + ((ActivityManager) getContext().getSystemService(Activity.ACTIVITY_SERVICE)). + killBackgroundProcesses(arg); + return null; + } } return super.call(method, arg, extras); } public static Bundle callCommand(String command) { + return callCommand(command, null); + } + + public static Bundle callCommand(String command, String arg) { Instrumentation inst = InstrumentationRegistry.getInstrumentation(); Uri uri = Uri.parse("content://" + inst.getContext().getPackageName() + ".commands"); - return inst.getTargetContext().getContentResolver().call(uri, command, null, null); + return inst.getTargetContext().getContentResolver().call(uri, command, arg, null); } }