From 30ac97d9386736c3322a3277f74523497627b9f5 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Wed, 24 Jun 2020 23:47:46 -0700 Subject: [PATCH] Adding home animation support for non-system Launcher When user swipes up to home, Launcher will receive a onNewIntent callwith a bundle-extra gesture_nav_contract_v1. It will contain the componentName & UserHandle of the closing app & a callback. Launcher can use the callback to return the final position where the app should animate to and an optional surface to be used for crossFade animation. The surface cleanup can be handled in onEnterAnimationComplete. Change-Id: I76fdd810fdcb80b71f7d7588ccac8976d9dfe278 --- .../quickstep/FallbackSwipeHandler.java | 138 +++++++++- .../launcher3/BaseQuickstepLauncher.java | 7 + res/layout/floating_surface_view.xml | 19 ++ .../launcher3/AbstractFloatingView.java | 9 +- src/com/android/launcher3/BubbleTextView.java | 14 +- .../android/launcher3/GestureNavContract.java | 101 ++++++++ src/com/android/launcher3/Launcher.java | 15 ++ .../launcher3/views/FloatingIconView.java | 20 +- .../launcher3/views/FloatingSurfaceView.java | 241 ++++++++++++++++++ 9 files changed, 546 insertions(+), 18 deletions(-) create mode 100644 res/layout/floating_surface_view.xml create mode 100644 src/com/android/launcher3/GestureNavContract.java create mode 100644 src/com/android/launcher3/views/FloatingSurfaceView.java diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java index 1909f4772e..f60a50bb43 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java @@ -15,14 +15,35 @@ */ package com.android.quickstep; +import static android.content.Intent.EXTRA_COMPONENT_NAME; +import static android.content.Intent.EXTRA_USER; + +import static com.android.launcher3.GestureNavContract.EXTRA_GESTURE_CONTRACT; +import static com.android.launcher3.GestureNavContract.EXTRA_ICON_POSITION; +import static com.android.launcher3.GestureNavContract.EXTRA_ICON_SURFACE; +import static com.android.launcher3.GestureNavContract.EXTRA_REMOTE_CALLBACK; import static com.android.launcher3.anim.Interpolators.ACCEL; import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME; import android.animation.ObjectAnimator; +import android.annotation.TargetApi; import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.ParcelUuid; +import android.os.UserHandle; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; import androidx.annotation.NonNull; @@ -32,19 +53,33 @@ import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.SpringAnimationBuilder; import com.android.quickstep.fallback.FallbackRecentsView; +import com.android.quickstep.util.RectFSpringAnim; import com.android.quickstep.util.TransformParams; import com.android.quickstep.util.TransformParams.BuilderProxy; +import com.android.systemui.shared.recents.model.Task.TaskKey; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; +import java.lang.ref.WeakReference; +import java.util.UUID; +import java.util.function.Consumer; + /** * Handles the navigation gestures when a 3rd party launcher is the default home activity. */ +@TargetApi(Build.VERSION_CODES.R) public class FallbackSwipeHandler extends BaseSwipeUpHandlerV2 { + /** + * Message used for receiving gesture nav contract information. We use a static messenger to + * avoid leaking too make binders in case the receiving launcher does not handle the contract + * properly. + */ + private static StaticMessageReceiver sMessageReceiver = null; + private FallbackHomeAnimationFactory mActiveAnimationFactory; private final boolean mRunningOverHome; @@ -89,7 +124,9 @@ public class FallbackSwipeHandler extends protected HomeAnimationFactory createHomeAnimationFactory(long duration) { mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration); ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0); - mContext.startActivity(new Intent(mGestureState.getHomeIntent()), options.toBundle()); + Intent intent = new Intent(mGestureState.getHomeIntent()); + mActiveAnimationFactory.addGestureContract(intent); + mContext.startActivity(intent, options.toBundle()); return mActiveAnimationFactory; } @@ -130,15 +167,19 @@ public class FallbackSwipeHandler extends } private class FallbackHomeAnimationFactory extends HomeAnimationFactory { - + private final Rect mTempRect = new Rect(); private final TransformParams mHomeAlphaParams = new TransformParams(); private final AnimatedFloat mHomeAlpha; private final AnimatedFloat mVerticalShiftForScale = new AnimatedFloat(); - private final AnimatedFloat mRecentsAlpha = new AnimatedFloat(); + private final RectF mTargetRect = new RectF(); + private SurfaceControl mSurfaceControl; + private final long mDuration; + + private RectFSpringAnim mSpringAnim; FallbackHomeAnimationFactory(long duration) { mDuration = duration; @@ -161,6 +202,15 @@ public class FallbackSwipeHandler extends this::updateRecentsActivityTransformDuringHomeAnim); } + @NonNull + @Override + public RectF getWindowTargetRect() { + if (mTargetRect.isEmpty()) { + mTargetRect.set(super.getWindowTargetRect()); + } + return mTargetRect; + } + private void updateRecentsActivityTransformDuringHomeAnim(SurfaceParams.Builder builder, RemoteAnimationTargetCompat app, TransformParams params) { builder.withAlpha(mRecentsAlpha.value); @@ -217,5 +267,87 @@ public class FallbackSwipeHandler extends .start(); } } + + @Override + public void setAnimation(RectFSpringAnim anim) { + mSpringAnim = anim; + } + + private void onMessageReceived(Message msg) { + try { + Bundle data = msg.getData(); + RectF position = data.getParcelable(EXTRA_ICON_POSITION); + if (!position.isEmpty()) { + mSurfaceControl = data.getParcelable(EXTRA_ICON_SURFACE); + mTargetRect.set(position); + if (mSpringAnim != null) { + mSpringAnim.onTargetPositionChanged(); + } + } + } catch (Exception e) { + // Ignore + } + } + + @Override + public void update(RectF currentRect, float progress, float radius) { + if (mSurfaceControl != null) { + currentRect.roundOut(mTempRect); + Transaction t = new Transaction(); + t.setGeometry(mSurfaceControl, null, mTempRect, Surface.ROTATION_0); + t.apply(); + } + } + + private void addGestureContract(Intent intent) { + if (mRunningOverHome || mGestureState.getRunningTask() == null) { + return; + } + + TaskKey key = new TaskKey(mGestureState.getRunningTask()); + if (key.getComponent() != null) { + if (sMessageReceiver == null) { + sMessageReceiver = new StaticMessageReceiver(); + } + + Bundle gestureNavContract = new Bundle(); + gestureNavContract.putParcelable(EXTRA_COMPONENT_NAME, key.getComponent()); + gestureNavContract.putParcelable(EXTRA_USER, UserHandle.of(key.userId)); + gestureNavContract.putParcelable(EXTRA_REMOTE_CALLBACK, + sMessageReceiver.newCallback(this::onMessageReceived)); + intent.putExtra(EXTRA_GESTURE_CONTRACT, gestureNavContract); + } + } + } + + private static class StaticMessageReceiver implements Handler.Callback { + + private final Messenger mMessenger = + new Messenger(new Handler(Looper.getMainLooper(), this)); + + private ParcelUuid mCurrentUID = new ParcelUuid(UUID.randomUUID()); + private WeakReference> mCurrentCallback = new WeakReference<>(null); + + public Message newCallback(Consumer callback) { + mCurrentUID = new ParcelUuid(UUID.randomUUID()); + mCurrentCallback = new WeakReference<>(callback); + + Message msg = Message.obtain(); + msg.replyTo = mMessenger; + msg.obj = mCurrentUID; + return msg; + } + + @Override + public boolean handleMessage(@NonNull Message message) { + if (mCurrentUID.equals(message.obj)) { + Consumer consumer = mCurrentCallback.get(); + if (consumer != null) { + consumer.accept(message); + return true; + } + } + return false; + } } } diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java index 6b941be471..235df425b4 100644 --- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java @@ -110,6 +110,13 @@ public abstract class BaseQuickstepLauncher extends Launcher .getHighResLoadingState().setVisible(true); } + @Override + protected void handleGestureContract(Intent intent) { + if (FeatureFlags.SEPARATE_RECENTS_ACTIVITY.get()) { + super.handleGestureContract(intent); + } + } + @Override public void onTrimMemory(int level) { super.onTrimMemory(level); diff --git a/res/layout/floating_surface_view.xml b/res/layout/floating_surface_view.xml new file mode 100644 index 0000000000..434e84f269 --- /dev/null +++ b/res/layout/floating_surface_view.xml @@ -0,0 +1,19 @@ + + + diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java index cd27a2ddca..ce37a30160 100644 --- a/src/com/android/launcher3/AbstractFloatingView.java +++ b/src/com/android/launcher3/AbstractFloatingView.java @@ -62,7 +62,8 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch TYPE_ALL_APPS_EDU, TYPE_TASK_MENU, - TYPE_OPTIONS_POPUP + TYPE_OPTIONS_POPUP, + TYPE_ICON_SURFACE }) @Retention(RetentionPolicy.SOURCE) public @interface FloatingViewType {} @@ -80,16 +81,18 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch // Popups related to quickstep UI public static final int TYPE_TASK_MENU = 1 << 10; public static final int TYPE_OPTIONS_POPUP = 1 << 11; + public static final int TYPE_ICON_SURFACE = 1 << 12; public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU - | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU; + | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU + | TYPE_ICON_SURFACE; // Type of popups which should be kept open during launcher rebind public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE - | TYPE_ALL_APPS_EDU; + | TYPE_ALL_APPS_EDU | TYPE_ICON_SURFACE; // Usually we show the back button when a floating view is open. Instead, hide for these types. public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 48819cbfa0..198f13d02a 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -614,6 +614,9 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, @Override public void setIconVisible(boolean visible) { mIsIconVisible = visible; + if (!mIsIconVisible) { + resetIconScale(); + } Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT); applyCompoundDrawables(icon); } @@ -753,11 +756,14 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, @Override public SafeCloseable prepareDrawDragView() { - if (getIcon() instanceof FastBitmapDrawable) { - FastBitmapDrawable icon = (FastBitmapDrawable) getIcon(); - icon.setScale(1f); - } + resetIconScale(); setForceHideDot(true); return () -> { }; } + + private void resetIconScale() { + if (mIcon instanceof FastBitmapDrawable) { + ((FastBitmapDrawable) mIcon).setScale(1f); + } + } } diff --git a/src/com/android/launcher3/GestureNavContract.java b/src/com/android/launcher3/GestureNavContract.java new file mode 100644 index 0000000000..2a7e629247 --- /dev/null +++ b/src/com/android/launcher3/GestureNavContract.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2020 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; + +import static android.content.Intent.EXTRA_COMPONENT_NAME; +import static android.content.Intent.EXTRA_USER; + +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.RectF; +import android.os.Build; +import android.os.Bundle; +import android.os.Message; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Log; +import android.view.SurfaceControl; + +import androidx.annotation.Nullable; + +/** + * Class to encapsulate the handshake protocol between Launcher and gestureNav. + */ +public class GestureNavContract { + + private static final String TAG = "GestureNavContract"; + + public static final String EXTRA_GESTURE_CONTRACT = "gesture_nav_contract_v1"; + public static final String EXTRA_ICON_POSITION = "gesture_nav_contract_icon_position"; + public static final String EXTRA_ICON_SURFACE = "gesture_nav_contract_surface_control"; + public static final String EXTRA_REMOTE_CALLBACK = "android.intent.extra.REMOTE_CALLBACK"; + + public final ComponentName componentName; + public final UserHandle user; + + private final Message mCallback; + + public GestureNavContract(ComponentName componentName, UserHandle user, Message callback) { + this.componentName = componentName; + this.user = user; + this.mCallback = callback; + } + + /** + * Sends the position information to the receiver + */ + @TargetApi(Build.VERSION_CODES.R) + public void sendEndPosition(RectF position, @Nullable SurfaceControl surfaceControl) { + Bundle result = new Bundle(); + result.putParcelable(EXTRA_ICON_POSITION, position); + result.putParcelable(EXTRA_ICON_SURFACE, surfaceControl); + + Message callback = Message.obtain(); + callback.copyFrom(mCallback); + callback.setData(result); + + try { + callback.replyTo.send(callback); + } catch (RemoteException e) { + Log.e(TAG, "Error sending icon position", e); + } + } + + /** + * Clears and returns the GestureNavContract if it was present in the intent. + */ + public static GestureNavContract fromIntent(Intent intent) { + if (!Utilities.ATLEAST_R) { + return null; + } + Bundle extras = intent.getBundleExtra(EXTRA_GESTURE_CONTRACT); + if (extras == null) { + return null; + } + intent.removeExtra(EXTRA_GESTURE_CONTRACT); + + ComponentName componentName = extras.getParcelable(EXTRA_COMPONENT_NAME); + UserHandle userHandle = extras.getParcelable(EXTRA_USER); + Message callback = extras.getParcelable(EXTRA_REMOTE_CALLBACK); + + if (componentName != null && userHandle != null && callback != null + && callback.replyTo != null) { + return new GestureNavContract(componentName, userHandle, callback); + } + return null; + } +} diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index d06ae7a9c5..4675362f64 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -21,6 +21,7 @@ import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; +import static com.android.launcher3.AbstractFloatingView.TYPE_ICON_SURFACE; import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE; import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR; import static com.android.launcher3.InstallShortcutReceiver.FLAG_DRAG_AND_DROP; @@ -168,6 +169,7 @@ import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.UiThreadHelper; import com.android.launcher3.util.ViewOnDrawExecutor; import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.FloatingSurfaceView; import com.android.launcher3.views.OptionsPopupView; import com.android.launcher3.views.ScrimView; import com.android.launcher3.widget.LauncherAppWidgetHostView; @@ -509,6 +511,7 @@ public class Launcher extends StatefulActivity implements Launche public void onEnterAnimationComplete() { super.onEnterAnimationComplete(); mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE); + AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE); } @Override @@ -1450,6 +1453,7 @@ public class Launcher extends StatefulActivity implements Launche mLauncherCallbacks.onHomeIntent(internalStateHandled); } mOverlayManager.hideOverlay(isStarted() && !isForceInvisible()); + handleGestureContract(intent); } else if (Intent.ACTION_ALL_APPS.equals(intent.getAction())) { getStateManager().goToState(ALL_APPS, alreadyOnHome); } @@ -1457,6 +1461,17 @@ public class Launcher extends StatefulActivity implements Launche TraceHelper.INSTANCE.endSection(traceToken); } + /** + * Handles gesture nav contract + */ + protected void handleGestureContract(Intent intent) { + GestureNavContract gnc = GestureNavContract.fromIntent(intent); + if (gnc != null) { + AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE); + FloatingSurfaceView.show(this, gnc); + } + } + /** * Hides the keyboard if visible */ diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java index 7cdde2e84b..8186dfa3dc 100644 --- a/src/com/android/launcher3/views/FloatingIconView.java +++ b/src/com/android/launcher3/views/FloatingIconView.java @@ -196,13 +196,18 @@ public class FloatingIconView extends FrameLayout implements layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height); } + private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, + RectF outRect) { + getLocationBoundsForView(launcher, v, isOpening, outRect, new Rect()); + } + /** * Gets the location bounds of a view and returns the overall rotation. * - For DeepShortcutView, we return the bounds of the icon view. * - For BubbleTextView, we return the icon bounds. */ - private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, - RectF outRect) { + public static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening, + RectF outRect, Rect outViewBounds) { boolean ignoreTransform = !isOpening; if (v instanceof DeepShortcutView) { v = ((DeepShortcutView) v).getBubbleText(); @@ -215,17 +220,16 @@ public class FloatingIconView extends FrameLayout implements return; } - Rect iconBounds = new Rect(); if (v instanceof BubbleTextView) { - ((BubbleTextView) v).getIconBounds(iconBounds); + ((BubbleTextView) v).getIconBounds(outViewBounds); } else if (v instanceof FolderIcon) { - ((FolderIcon) v).getPreviewBounds(iconBounds); + ((FolderIcon) v).getPreviewBounds(outViewBounds); } else { - iconBounds.set(0, 0, v.getWidth(), v.getHeight()); + outViewBounds.set(0, 0, v.getWidth(), v.getHeight()); } - float[] points = new float[] {iconBounds.left, iconBounds.top, iconBounds.right, - iconBounds.bottom}; + float[] points = new float[] {outViewBounds.left, outViewBounds.top, outViewBounds.right, + outViewBounds.bottom}; Utilities.getDescendantCoordRelativeToAncestor(v, launcher.getDragLayer(), points, false, ignoreTransform); outRect.set( diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java new file mode 100644 index 0000000000..040619e9c7 --- /dev/null +++ b/src/com/android/launcher3/views/FloatingSurfaceView.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2020 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.views; + +import static com.android.launcher3.views.FloatingIconView.getLocationBoundsForView; +import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Picture; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Build; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; + +import androidx.annotation.NonNull; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.GestureNavContract; +import com.android.launcher3.Insettable; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.launcher3.util.DefaultDisplay; +import com.android.launcher3.util.Executors; + +/** + * Similar to {@link FloatingIconView} but displays a surface with the targetIcon. It then passes + * the surfaceHandle to the {@link GestureNavContract}. + */ +@TargetApi(Build.VERSION_CODES.R) +public class FloatingSurfaceView extends AbstractFloatingView implements + OnGlobalLayoutListener, Insettable, SurfaceHolder.Callback2 { + + private final RectF mTmpPosition = new RectF(); + + private final Launcher mLauncher; + private final RectF mIconPosition = new RectF(); + + private final Rect mIconBounds = new Rect(); + private final Picture mPicture = new Picture(); + private final Runnable mRemoveViewRunnable = this::removeViewFromParent; + + private final SurfaceView mSurfaceView; + + + private View mIcon; + private GestureNavContract mContract; + + public FloatingSurfaceView(Context context) { + this(context, null); + } + + public FloatingSurfaceView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FloatingSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLauncher = Launcher.getLauncher(context); + + mSurfaceView = new SurfaceView(context); + mSurfaceView.setZOrderOnTop(true); + + mSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT); + mSurfaceView.getHolder().addCallback(this); + mIsOpen = true; + addView(mSurfaceView); + } + + @Override + protected void handleClose(boolean animate) { + setCurrentIconVisible(true); + mLauncher.getViewCache().recycleView(R.layout.floating_surface_view, this); + mContract = null; + mIcon = null; + mIsOpen = false; + + // Remove after some time, to avoid flickering + Executors.MAIN_EXECUTOR.getHandler().postDelayed(mRemoveViewRunnable, + DefaultDisplay.INSTANCE.get(mLauncher).getInfo().singleFrameMs); + } + + private void removeViewFromParent() { + mPicture.beginRecording(1, 1); + mPicture.endRecording(); + mLauncher.getDragLayer().removeView(this); + } + + /** + * Shows the surfaceView for the provided contract + */ + public static void show(Launcher launcher, GestureNavContract contract) { + FloatingSurfaceView view = launcher.getViewCache().getView(R.layout.floating_surface_view, + launcher, launcher.getDragLayer()); + view.mContract = contract; + view.mIsOpen = true; + + // Cancel any pending remove + Executors.MAIN_EXECUTOR.getHandler().removeCallbacks(view.mRemoveViewRunnable); + view.removeViewFromParent(); + launcher.getDragLayer().addView(view); + } + + @Override + public void logActionCommand(int command) { } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_ICON_SURFACE) != 0; + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + close(false); + return false; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + getViewTreeObserver().addOnGlobalLayoutListener(this); + updateIconLocation(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + getViewTreeObserver().removeOnGlobalLayoutListener(this); + setCurrentIconVisible(true); + } + + @Override + public void onGlobalLayout() { + updateIconLocation(); + } + + @Override + public void setInsets(Rect insets) { } + + private void updateIconLocation() { + if (mContract == null) { + return; + } + View icon = mLauncher.getWorkspace().getFirstMatchForAppClose( + mContract.componentName.getPackageName(), mContract.user); + + boolean iconChanged = mIcon != icon; + if (iconChanged) { + setCurrentIconVisible(true); + mIcon = icon; + setCurrentIconVisible(false); + } + + if (icon != null && icon.isAttachedToWindow()) { + getLocationBoundsForView(mLauncher, icon, false, mTmpPosition, mIconBounds); + + if (!mTmpPosition.equals(mIconPosition)) { + mIconPosition.set(mTmpPosition); + sendIconInfo(); + + LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams(); + lp.width = Math.round(mIconPosition.width()); + lp.height = Math.round(mIconPosition.height()); + lp.leftMargin = Math.round(mIconPosition.left); + lp.topMargin = Math.round(mIconPosition.top); + } + } + if (iconChanged && !mIconBounds.isEmpty()) { + // Record the icon display + setCurrentIconVisible(true); + Canvas c = mPicture.beginRecording(mIconBounds.width(), mIconBounds.height()); + c.translate(-mIconBounds.left, -mIconBounds.top); + mIcon.draw(c); + mPicture.endRecording(); + setCurrentIconVisible(false); + drawOnSurface(); + } + } + + private void sendIconInfo() { + if (mContract != null && !mIconPosition.isEmpty()) { + mContract.sendEndPosition(mIconPosition, mSurfaceView.getSurfaceControl()); + } + } + + @Override + public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) { + drawOnSurface(); + sendIconInfo(); + } + + @Override + public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, + int format, int width, int height) { + drawOnSurface(); + } + + @Override + public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {} + + @Override + public void surfaceRedrawNeeded(@NonNull SurfaceHolder surfaceHolder) { + drawOnSurface(); + } + + private void drawOnSurface() { + SurfaceHolder surfaceHolder = mSurfaceView.getHolder(); + + Canvas c = surfaceHolder.lockHardwareCanvas(); + if (c != null) { + mPicture.draw(c); + surfaceHolder.unlockCanvasAndPost(c); + } + } + + private void setCurrentIconVisible(boolean isVisible) { + if (mIcon != null) { + setIconAndDotVisible(mIcon, isVisible); + } + } +}