diff options
Diffstat (limited to 'libs')
38 files changed, 1268 insertions, 689 deletions
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml index 501bedd50f55..c2755ef6ccb6 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml @@ -19,6 +19,7 @@ android:layout_height="wrap_content" android:layout_width="wrap_content" android:orientation="vertical" + android:clipChildren="false" android:id="@+id/bubble_expanded_view"> <com.android.wm.shell.bubbles.bar.BubbleBarHandleView diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml index f1ecde49ce78..7aca921dccc7 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml @@ -14,20 +14,18 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<com.android.wm.shell.bubbles.bar.BubbleBarMenuView - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" +<com.android.wm.shell.bubbles.bar.BubbleBarMenuView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" + android:clipToPadding="false" android:minWidth="@dimen/bubble_bar_manage_menu_min_width" android:orientation="vertical" - android:elevation="@dimen/bubble_manage_menu_elevation" - android:paddingTop="@dimen/bubble_bar_manage_menu_padding_top" - android:paddingHorizontal="@dimen/bubble_bar_manage_menu_padding" - android:paddingBottom="@dimen/bubble_bar_manage_menu_padding" - android:clipToPadding="false"> + android:visibility="invisible" + tools:visibility="visible"> <LinearLayout android:id="@+id/bubble_bar_manage_menu_bubble_section" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 2a50e4d0d74b..272dfecb0bf9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -222,7 +222,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mHandleView.setAccessibilityDelegate(new HandleViewAccessibilityDelegate()); } - mMenuViewController = new BubbleBarMenuViewController(mContext, this); + mMenuViewController = new BubbleBarMenuViewController(mContext, mHandleView, this); mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() { @Override public void onMenuVisibilityChanged(boolean visible) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java index e781c07f01a7..712e41b0b3c5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java @@ -17,17 +17,18 @@ package com.android.wm.shell.bubbles.bar; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.annotation.Nullable; import android.content.Context; -import android.graphics.Outline; -import android.graphics.Path; -import android.graphics.RectF; +import android.graphics.Canvas; +import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; -import android.view.ViewOutlineProvider; import androidx.annotation.ColorInt; +import androidx.annotation.VisibleForTesting; +import androidx.core.animation.IntProperty; import androidx.core.content.ContextCompat; import com.android.wm.shell.R; @@ -37,14 +38,33 @@ import com.android.wm.shell.R; */ public class BubbleBarHandleView extends View { private static final long COLOR_CHANGE_DURATION = 120; - // Path used to draw the dots - private final Path mPath = new Path(); + /** Custom property to set handle color. */ + private static final IntProperty<BubbleBarHandleView> HANDLE_COLOR = new IntProperty<>( + "handleColor") { + @Override + public void setValue(BubbleBarHandleView bubbleBarHandleView, int color) { + bubbleBarHandleView.setHandleColor(color); + } + + @Override + public Integer get(BubbleBarHandleView bubbleBarHandleView) { + return bubbleBarHandleView.getHandleColor(); + } + }; + + @VisibleForTesting + final Paint mHandlePaint = new Paint(); private final @ColorInt int mHandleLightColor; private final @ColorInt int mHandleDarkColor; - private @ColorInt int mCurrentColor; + private final ArgbEvaluator mArgbEvaluator = ArgbEvaluator.getInstance(); + private final float mHandleHeight; + private final float mHandleWidth; + private float mCurrentHandleHeight; + private float mCurrentHandleWidth; @Nullable private ObjectAnimator mColorChangeAnim; + private @ColorInt int mRegionSamplerColor; public BubbleBarHandleView(Context context) { this(context, null /* attrs */); @@ -61,30 +81,52 @@ public class BubbleBarHandleView extends View { public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - final int handleHeight = getResources().getDimensionPixelSize( + mHandlePaint.setFlags(Paint.ANTI_ALIAS_FLAG); + mHandlePaint.setStyle(Paint.Style.FILL); + mHandlePaint.setColor(0); + mHandleHeight = getResources().getDimensionPixelSize( R.dimen.bubble_bar_expanded_view_handle_height); + mHandleWidth = getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_caption_width); mHandleLightColor = ContextCompat.getColor(getContext(), R.color.bubble_bar_expanded_view_handle_light); mHandleDarkColor = ContextCompat.getColor(getContext(), R.color.bubble_bar_expanded_view_handle_dark); - - setClipToOutline(true); - setOutlineProvider(new ViewOutlineProvider() { - @Override - public void getOutline(View view, Outline outline) { - final int handleCenterY = view.getHeight() / 2; - final int handleTop = handleCenterY - handleHeight / 2; - final int handleBottom = handleTop + handleHeight; - final int radius = handleHeight / 2; - RectF handle = new RectF(/* left = */ 0, handleTop, view.getWidth(), handleBottom); - mPath.reset(); - mPath.addRoundRect(handle, radius, radius, Path.Direction.CW); - outline.setPath(mPath); - } - }); + mCurrentHandleHeight = mHandleHeight; + mCurrentHandleWidth = mHandleWidth; setContentDescription(getResources().getString(R.string.handle_text)); } + private void setHandleColor(int color) { + mHandlePaint.setColor(color); + invalidate(); + } + + private int getHandleColor() { + return mHandlePaint.getColor(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + float handleLeft = (getWidth() - mCurrentHandleWidth) / 2; + float handleRight = handleLeft + mCurrentHandleWidth; + float handleCenterY = (float) getHeight() / 2; + float handleTop = (int) (handleCenterY - mCurrentHandleHeight / 2); + float handleBottom = handleTop + mCurrentHandleHeight; + float cornerRadius = mCurrentHandleHeight / 2; + canvas.drawRoundRect(handleLeft, handleTop, handleRight, handleBottom, cornerRadius, + cornerRadius, mHandlePaint); + } + + /** Sets handle width, height and color. Does not change the layout properties */ + private void setHandleProperties(float width, float height, int color) { + mCurrentHandleHeight = height; + mCurrentHandleWidth = width; + mHandlePaint.setColor(color); + invalidate(); + } + /** * Updates the handle color. * @@ -94,15 +136,15 @@ public class BubbleBarHandleView extends View { */ public void updateHandleColor(boolean isRegionDark, boolean animated) { int newColor = isRegionDark ? mHandleLightColor : mHandleDarkColor; - if (newColor == mCurrentColor) { + if (newColor == mRegionSamplerColor) { return; } + mRegionSamplerColor = newColor; if (mColorChangeAnim != null) { mColorChangeAnim.cancel(); } - mCurrentColor = newColor; if (animated) { - mColorChangeAnim = ObjectAnimator.ofArgb(this, "backgroundColor", newColor); + mColorChangeAnim = ObjectAnimator.ofArgb(this, HANDLE_COLOR, newColor); mColorChangeAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -112,7 +154,39 @@ public class BubbleBarHandleView extends View { mColorChangeAnim.setDuration(COLOR_CHANGE_DURATION); mColorChangeAnim.start(); } else { - setBackgroundColor(newColor); + setHandleColor(newColor); } } + + /** Returns handle padding top. */ + public int getHandlePaddingTop() { + return (getHeight() - getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_handle_height)) / 2; + } + + /** Animates handle for the bubble menu. */ + public void animateHandleForMenu(float progress, float widthDelta, float heightDelta, + int menuColor) { + float currentWidth = mHandleWidth + widthDelta * progress; + float currentHeight = mHandleHeight + heightDelta * progress; + int color = (int) mArgbEvaluator.evaluate(progress, mRegionSamplerColor, menuColor); + setHandleProperties(currentWidth, currentHeight, color); + setTranslationY(heightDelta * progress / 2); + } + + /** Restores all the properties that were animated to the default values. */ + public void restoreAnimationDefaults() { + setHandleProperties(mHandleWidth, mHandleHeight, mRegionSamplerColor); + setTranslationY(0); + } + + /** Returns the handle height. */ + public int getHandleHeight() { + return (int) mHandleHeight; + } + + /** Returns the handle width. */ + public int getHandleWidth() { + return (int) mHandleWidth; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java index 0ee20ef1731f..99e20097e61c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java @@ -47,6 +47,10 @@ public class BubbleBarMenuView extends LinearLayout { private ImageView mBubbleIconView; private ImageView mBubbleDismissIconView; private TextView mBubbleTitleView; + // The animation has three stages. Each stage transition lasts until the animation ends. In + // stage 1, the title item content fades in. In stage 2, the background of the option items + // fades in. In stage 3, the option item content fades in. + private static final int SHOW_MENU_STAGES_COUNT = 3; public BubbleBarMenuView(Context context) { this(context, null /* attrs */); @@ -97,6 +101,35 @@ public class BubbleBarMenuView extends LinearLayout { } } + /** Animates the menu from the specified start scale. */ + public void animateFromStartScale(float currentScale, float progress) { + int menuItemElevation = getResources().getDimensionPixelSize( + R.dimen.bubble_manage_menu_elevation); + setScaleX(currentScale); + setScaleY(currentScale); + setAlphaForTitleViews(progress); + mBubbleSectionView.setElevation(menuItemElevation * progress); + float actionsBackgroundAlpha = Math.max(0, + (progress - (float) 1 / SHOW_MENU_STAGES_COUNT) * (SHOW_MENU_STAGES_COUNT - 1)); + float actionItemsAlpha = Math.max(0, + (progress - (float) 2 / SHOW_MENU_STAGES_COUNT) * SHOW_MENU_STAGES_COUNT); + mActionsSectionView.setAlpha(actionsBackgroundAlpha); + mActionsSectionView.setElevation(menuItemElevation * actionsBackgroundAlpha); + setMenuItemViewsAlpha(actionItemsAlpha); + } + + private void setAlphaForTitleViews(float alpha) { + mBubbleIconView.setAlpha(alpha); + mBubbleTitleView.setAlpha(alpha); + mBubbleDismissIconView.setAlpha(alpha); + } + + private void setMenuItemViewsAlpha(float alpha) { + for (int i = mActionsSectionView.getChildCount() - 1; i >= 0; i--) { + mActionsSectionView.getChildAt(i).setAlpha(alpha); + } + } + /** Update menu details with bubble info */ void updateInfo(Bubble bubble) { if (bubble.getIcon() != null) { @@ -153,6 +186,11 @@ public class BubbleBarMenuView extends LinearLayout { return mBubbleSectionView.getAlpha(); } + /** Return title menu item height. */ + public float getTitleItemHeight() { + return mBubbleSectionView.getHeight(); + } + /** * Menu action details used to create menu items */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java index 514810745e10..9dd0cae20370 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java @@ -15,6 +15,9 @@ */ package com.android.wm.shell.bubbles.bar; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -26,13 +29,10 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import androidx.dynamicanimation.animation.DynamicAnimation; -import androidx.dynamicanimation.animation.SpringForce; - +import com.android.app.animation.Interpolators; import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; -import com.android.wm.shell.shared.animation.PhysicsAnimator; import java.util.ArrayList; @@ -40,22 +40,26 @@ import java.util.ArrayList; * Manages bubble bar expanded view menu presentation and animations */ class BubbleBarMenuViewController { - private static final float MENU_INITIAL_SCALE = 0.5f; + + private static final float WIDTH_SWAP_FRACTION = 0.4F; + private static final long MENU_ANIMATION_DURATION = 600; + private final Context mContext; private final ViewGroup mRootView; + private final BubbleBarHandleView mHandleView; private @Nullable Listener mListener; private @Nullable Bubble mBubble; private @Nullable BubbleBarMenuView mMenuView; /** A transparent view used to intercept touches to collapse menu when presented */ private @Nullable View mScrimView; - private @Nullable PhysicsAnimator<BubbleBarMenuView> mMenuAnimator; - private PhysicsAnimator.SpringConfig mMenuSpringConfig; + private @Nullable ValueAnimator mMenuAnimator; + - BubbleBarMenuViewController(Context context, ViewGroup rootView) { + BubbleBarMenuViewController(Context context, BubbleBarHandleView handleView, + ViewGroup rootView) { mContext = context; mRootView = rootView; - mMenuSpringConfig = new PhysicsAnimator.SpringConfig( - SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); + mHandleView = handleView; } /** Tells if the menu is visible or being animated */ @@ -81,20 +85,21 @@ class BubbleBarMenuViewController { if (mMenuView == null || mScrimView == null) { setupMenu(); } - cancelAnimations(); - mMenuView.setVisibility(View.VISIBLE); - mScrimView.setVisibility(View.VISIBLE); - Runnable endActions = () -> { - mMenuView.getChildAt(0).requestAccessibilityFocus(); - if (mListener != null) { - mListener.onMenuVisibilityChanged(true /* isShown */); + runOnMenuIsMeasured(() -> { + mMenuView.setVisibility(View.VISIBLE); + mScrimView.setVisibility(View.VISIBLE); + Runnable endActions = () -> { + mMenuView.getChildAt(0).requestAccessibilityFocus(); + if (mListener != null) { + mListener.onMenuVisibilityChanged(true /* isShown */); + } + }; + if (animated) { + animateTransition(true /* show */, endActions); + } else { + endActions.run(); } - }; - if (animated) { - animateTransition(true /* show */, endActions); - } else { - endActions.run(); - } + }); } /** @@ -103,18 +108,30 @@ class BubbleBarMenuViewController { */ void hideMenu(boolean animated) { if (mMenuView == null || mScrimView == null) return; - cancelAnimations(); - Runnable endActions = () -> { - mMenuView.setVisibility(View.GONE); - mScrimView.setVisibility(View.GONE); - if (mListener != null) { - mListener.onMenuVisibilityChanged(false /* isShown */); + runOnMenuIsMeasured(() -> { + Runnable endActions = () -> { + mHandleView.restoreAnimationDefaults(); + mMenuView.setVisibility(View.GONE); + mScrimView.setVisibility(View.GONE); + mHandleView.setVisibility(View.VISIBLE); + if (mListener != null) { + mListener.onMenuVisibilityChanged(false /* isShown */); + } + }; + if (animated) { + animateTransition(false /* show */, endActions); + } else { + endActions.run(); } - }; - if (animated) { - animateTransition(false /* show */, endActions); + }); + } + + private void runOnMenuIsMeasured(Runnable action) { + if (mMenuView.getWidth() == 0 || mMenuView.getHeight() == 0) { + // the menu view is not yet measured, postpone showing the animation + mMenuView.post(() -> runOnMenuIsMeasured(action)); } else { - endActions.run(); + action.run(); } } @@ -125,24 +142,63 @@ class BubbleBarMenuViewController { */ private void animateTransition(boolean show, Runnable endActions) { if (mMenuView == null) return; - mMenuAnimator = PhysicsAnimator.getInstance(mMenuView); - mMenuAnimator.setDefaultSpringConfig(mMenuSpringConfig); - mMenuAnimator - .spring(DynamicAnimation.ALPHA, show ? 1f : 0f) - .spring(DynamicAnimation.SCALE_Y, show ? 1f : MENU_INITIAL_SCALE) - .withEndActions(() -> { - mMenuAnimator = null; - endActions.run(); - }) - .start(); + float startValue = show ? 0 : 1; + if (mMenuAnimator != null && mMenuAnimator.isRunning()) { + startValue = (float) mMenuAnimator.getAnimatedValue(); + mMenuAnimator.cancel(); + } + ValueAnimator showMenuAnimation = ValueAnimator.ofFloat(startValue, show ? 1 : 0); + showMenuAnimation.setDuration(MENU_ANIMATION_DURATION); + showMenuAnimation.setInterpolator(Interpolators.EMPHASIZED); + showMenuAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mMenuAnimator = null; + endActions.run(); + } + }); + mMenuAnimator = showMenuAnimation; + setupAnimatorListener(showMenuAnimation); + showMenuAnimation.start(); } - /** Cancel running animations */ - private void cancelAnimations() { - if (mMenuAnimator != null) { - mMenuAnimator.cancel(); - mMenuAnimator = null; + /** Setup listener that orchestrates the animation. */ + private void setupAnimatorListener(ValueAnimator showMenuAnimation) { + // Getting views properties start values + int widthDiff = mMenuView.getWidth() - mHandleView.getHandleWidth(); + int handleHeight = mHandleView.getHandleHeight(); + float targetWidth = mHandleView.getHandleWidth() + widthDiff * WIDTH_SWAP_FRACTION; + float targetHeight = targetWidth * mMenuView.getTitleItemHeight() / mMenuView.getWidth(); + int menuColor; + try (TypedArray ta = mContext.obtainStyledAttributes(new int[]{ + com.android.internal.R.attr.materialColorSurfaceBright, + })) { + menuColor = ta.getColor(0, Color.WHITE); } + // Calculating deltas + float swapScale = targetWidth / mMenuView.getWidth(); + float handleWidthDelta = targetWidth - mHandleView.getHandleWidth(); + float handleHeightDelta = targetHeight - handleHeight; + // Setting update listener that will orchestrate the animation + showMenuAnimation.addUpdateListener(animator -> { + float animationProgress = (float) animator.getAnimatedValue(); + boolean showHandle = animationProgress <= WIDTH_SWAP_FRACTION; + mHandleView.setVisibility(showHandle ? View.VISIBLE : View.GONE); + mMenuView.setVisibility(showHandle ? View.GONE : View.VISIBLE); + if (showHandle) { + float handleAnimationProgress = animationProgress / WIDTH_SWAP_FRACTION; + mHandleView.animateHandleForMenu(handleAnimationProgress, handleWidthDelta, + handleHeightDelta, menuColor); + } else { + mMenuView.setTranslationY(mHandleView.getHandlePaddingTop()); + mMenuView.setPivotY(0); + mMenuView.setPivotX((float) mMenuView.getWidth() / 2); + float menuAnimationProgress = + (animationProgress - WIDTH_SWAP_FRACTION) / (1 - WIDTH_SWAP_FRACTION); + float currentMenuScale = swapScale + (1 - swapScale) * menuAnimationProgress; + mMenuView.animateFromStartScale(currentMenuScale, menuAnimationProgress); + } + }); } /** Sets up and inflate menu views */ @@ -150,9 +206,6 @@ class BubbleBarMenuViewController { // Menu view setup mMenuView = (BubbleBarMenuView) LayoutInflater.from(mContext).inflate( R.layout.bubble_bar_menu_view, mRootView, false); - mMenuView.setAlpha(0f); - mMenuView.setPivotY(0f); - mMenuView.setScaleY(MENU_INITIAL_SCALE); mMenuView.setOnCloseListener(() -> hideMenu(true /* animated */)); if (mBubble != null) { mMenuView.updateInfo(mBubble); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt index 4abb35c2a428..193c593e2ab2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipAppOpsListener.kt @@ -16,8 +16,11 @@ package com.android.wm.shell.common.pip import android.app.AppOpsManager +import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager +import android.util.Pair +import com.android.internal.annotations.VisibleForTesting import com.android.wm.shell.common.ShellExecutor class PipAppOpsListener( @@ -27,10 +30,12 @@ class PipAppOpsListener( ) { private val mAppOpsManager: AppOpsManager = checkNotNull( mContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager) + private var mTopPipActivityInfoSupplier: (Context) -> Pair<ComponentName?, Int> = + PipUtils::getTopPipActivity private val mAppOpsChangedListener = AppOpsManager.OnOpChangedListener { _, packageName -> try { // Dismiss the PiP once the user disables the app ops setting for that package - val topPipActivityInfo = PipUtils.getTopPipActivity(mContext) + val topPipActivityInfo = mTopPipActivityInfoSupplier.invoke(mContext) val componentName = topPipActivityInfo.first ?: return@OnOpChangedListener val userId = topPipActivityInfo.second val appInfo = mContext.packageManager @@ -75,4 +80,9 @@ class PipAppOpsListener( /** Dismisses the PIP window. */ fun dismissPip() } + + @VisibleForTesting + fun setTopPipActivityInfoSupplier(supplier: (Context) -> Pair<ComponentName?, Int>) { + mTopPipActivityInfoSupplier = supplier + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java index 3a4764d45f2c..3cd5df3121c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java @@ -19,6 +19,7 @@ package com.android.wm.shell.dagger.pip; import android.content.Context; import android.os.Handler; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; @@ -41,6 +42,7 @@ import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipParamsChangedForwarder; @@ -169,6 +171,8 @@ public abstract class Pip1Module { PipParamsChangedForwarder pipParamsChangedForwarder, Optional<SplitScreenController> splitScreenControllerOptional, Optional<PipPerfHintController> pipPerfHintControllerOptional, + Optional<DesktopRepository> desktopRepositoryOptional, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { @@ -176,7 +180,8 @@ public abstract class Pip1Module { syncTransactionQueue, pipTransitionState, pipBoundsState, pipDisplayLayoutState, pipBoundsAlgorithm, menuPhoneController, pipAnimationController, pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder, - splitScreenControllerOptional, pipPerfHintControllerOptional, displayController, + splitScreenControllerOptional, pipPerfHintControllerOptional, + desktopRepositoryOptional, rootTaskDisplayAreaOrganizer, displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java index 8d1b15c1e631..78e676f8cd45 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java @@ -22,6 +22,7 @@ import android.os.SystemClock; import androidx.annotation.NonNull; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; @@ -214,6 +215,7 @@ public abstract class TvPipModule { PipSurfaceTransactionHelper pipSurfaceTransactionHelper, Optional<SplitScreenController> splitScreenControllerOptional, Optional<PipPerfHintController> pipPerfHintControllerOptional, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { @@ -221,8 +223,9 @@ public abstract class TvPipModule { syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipDisplayLayoutState, tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder, - splitScreenControllerOptional, pipPerfHintControllerOptional, displayController, - pipUiEventLogger, shellTaskOrganizer, mainExecutor); + splitScreenControllerOptional, pipPerfHintControllerOptional, + rootTaskDisplayAreaOrganizer, displayController, pipUiEventLogger, + shellTaskOrganizer, mainExecutor); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index cefcb757690f..01c680dc8325 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -205,11 +205,6 @@ class DesktopMixedTransitionHandler( finishTransaction: SurfaceControl.Transaction, finishCallback: TransitionFinishCallback, ): Boolean { - val launchChange = findDesktopTaskChange(info, pending.launchingTask) - if (launchChange == null) { - logV("No launch Change, returning") - return false - } // Check if there's also an immersive change during this launch. val immersiveExitChange = pending.exitingImmersiveTask?.let { exitingTask -> findDesktopTaskChange(info, exitingTask) @@ -217,6 +212,13 @@ class DesktopMixedTransitionHandler( val minimizeChange = pending.minimizingTask?.let { minimizingTask -> findDesktopTaskChange(info, minimizingTask) } + val launchChange = findDesktopTaskChange(info, pending.launchingTask) + if (launchChange == null) { + check(minimizeChange == null) + check(immersiveExitChange == null) + logV("No launch Change, returning") + return false + } var subAnimationCount = -1 var combinedWct: WindowContainerTransaction? = null diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index fda709a4a2d7..08ca55f93e3f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -102,6 +102,9 @@ class DesktopRepository ( /* Tracks last bounds of task before toggled to stable bounds. */ private val boundsBeforeMaximizeByTaskId = SparseArray<Rect>() + /* Tracks last bounds of task before it is minimized. */ + private val boundsBeforeMinimizeByTaskId = SparseArray<Rect>() + /* Tracks last bounds of task before toggled to immersive state. */ private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>() @@ -462,6 +465,14 @@ class DesktopRepository ( fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) = boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds)) + /** Removes and returns the bounds saved before minimizing the given task. */ + fun removeBoundsBeforeMinimize(taskId: Int): Rect? = + boundsBeforeMinimizeByTaskId.removeReturnOld(taskId) + + /** Saves the bounds of the given task before minimizing. */ + fun saveBoundsBeforeMinimize(taskId: Int, bounds: Rect?) = + boundsBeforeMinimizeByTaskId.set(taskId, Rect(bounds)) + /** Removes and returns the bounds saved before entering immersive with the given task. */ fun removeBoundsBeforeFullImmersive(taskId: Int): Rect? = boundsBeforeFullImmersiveByTaskId.removeReturnOld(taskId) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 162879c97a16..927fd88fb4ff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -1770,9 +1770,13 @@ class DesktopTasksController( transition: IBinder, taskIdToMinimize: Int, ) { - val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) ?: return + val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize) desktopTasksLimiter.ifPresent { - it.addPendingMinimizeChange(transition, taskToMinimize.displayId, taskToMinimize.taskId) + it.addPendingMinimizeChange( + transition = transition, + displayId = taskToMinimize?.displayId ?: DEFAULT_DISPLAY, + taskId = taskIdToMinimize + ) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index f0e3a2bd8ffc..77af627a948a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -92,6 +92,12 @@ class DesktopTasksLimiter ( } taskToMinimize.transitionInfo = info activeTransitionTokensAndTasks[transition] = taskToMinimize + + // Save current bounds before minimizing in case we need to restore to it later. + val boundsBeforeMinimize = info.changes.find { change -> + change.taskInfo?.taskId == taskToMinimize.taskId }?.startAbsBounds + taskRepository.saveBoundsBeforeMinimize(taskToMinimize.taskId, boundsBeforeMinimize) + this@DesktopTasksLimiter.minimizeTask( taskToMinimize.displayId, taskToMinimize.taskId) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index b27c428f1693..0154d0455e50 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -45,12 +45,17 @@ public interface Pip { } /** - * Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed. + * Set the callback when isInPip state is changed. * - * @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()} - * when it's changed. + * @param callback The callback accepts the state of isInPip when it's changed. */ - default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {} + default void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {} + + /** + * Remove the callback when isInPip state is changed. + * @param callback The callback accepts the state of isInPip when it's changed. + */ + default void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) {} /** * Called when showing Pip menu. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index c4e63dfdade9..86c826a680f6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -17,6 +17,7 @@ package com.android.wm.shell.pip; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; @@ -67,6 +68,7 @@ import android.view.Choreographer; import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; +import android.window.DisplayAreaInfo; import android.window.TaskOrganizer; import android.window.TaskSnapshot; import android.window.WindowContainerToken; @@ -74,7 +76,9 @@ import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; +import com.android.window.flags.Flags; import com.android.wm.shell.R; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ScreenshotUtils; @@ -87,6 +91,7 @@ import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipPerfHintController; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.Interpolators; @@ -145,6 +150,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; private final Optional<SplitScreenController> mSplitScreenOptional; @Nullable private final PipPerfHintController mPipPerfHintController; + private final Optional<DesktopRepository> mDesktopRepositoryOptional; + private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; protected final ShellTaskOrganizer mTaskOrganizer; protected final ShellExecutor mMainExecutor; @@ -388,6 +395,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @NonNull PipParamsChangedForwarder pipParamsChangedForwarder, Optional<SplitScreenController> splitScreenOptional, Optional<PipPerfHintController> pipPerfHintControllerOptional, + Optional<DesktopRepository> desktopRepositoryOptional, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @NonNull ShellTaskOrganizer shellTaskOrganizer, @@ -414,6 +423,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); mSplitScreenOptional = splitScreenOptional; mPipPerfHintController = pipPerfHintControllerOptional.orElse(null); + mDesktopRepositoryOptional = desktopRepositoryOptional; + mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mTaskOrganizer = shellTaskOrganizer; mMainExecutor = mainExecutor; @@ -741,10 +752,23 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** Returns the bounds to restore to when exiting PIP mode. */ + // TODO(b/377581840): Instead of manually tracking bounds, use bounds from Core. public Rect getExitDestinationBounds() { + if (isPipLaunchedInDesktopMode()) { + final Rect freeformBounds = mDesktopRepositoryOptional.get().removeBoundsBeforeMinimize( + mTaskInfo.taskId); + return Objects.requireNonNullElseGet(freeformBounds, mPipBoundsState::getDisplayBounds); + } return mPipBoundsState.getDisplayBounds(); } + /** Returns whether PiP was launched while in desktop mode. */ + // TODO(377581840): Update this check to include non-minimized cases, e.g. split to PiP etc. + private boolean isPipLaunchedInDesktopMode() { + return Flags.enableDesktopWindowingPip() && mDesktopRepositoryOptional.isPresent() + && mDesktopRepositoryOptional.get().isMinimizedTask(mTaskInfo.taskId); + } + private void exitLaunchIntoPipTask(WindowContainerTransaction wct) { wct.startTask(mTaskInfo.launchIntoPipHostTaskId, null /* ActivityOptions */); mTaskOrganizer.applyTransaction(wct); @@ -1808,7 +1832,25 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * and can be overridden to restore to an alternate windowing mode. */ public int getOutPipWindowingMode() { - // By default, simply reset the windowing mode to undefined. + final DisplayAreaInfo tdaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo( + mTaskInfo.displayId); + + // If PiP was launched while in desktop mode (we should return the task to freeform + // windowing mode): + // 1) If the display windowing mode is freeform, set windowing mode to undefined so it will + // resolve the windowing mode to the display's windowing mode. + // 2) If the display windowing mode is not freeform, set windowing mode to freeform. + if (tdaInfo != null && isPipLaunchedInDesktopMode()) { + final int displayWindowingMode = + tdaInfo.configuration.windowConfiguration.getWindowingMode(); + if (displayWindowingMode == WINDOWING_MODE_FREEFORM) { + return WINDOWING_MODE_UNDEFINED; + } else { + return WINDOWING_MODE_FREEFORM; + } + } + + // By default, or if the task is going to fullscreen, reset the windowing mode to undefined. return WINDOWING_MODE_UNDEFINED; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 28b91c6cb812..8220ea5ea575 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -530,6 +530,13 @@ public class PipTransition extends PipTransitionController { if (mFixedRotationState != FIXED_ROTATION_TRANSITION && mFinishTransaction != null) { mFinishTransaction.merge(tx); + // Set window crop and position to destination bounds to avoid flickering. + if (hasValidLeash) { + mFinishTransaction.setWindowCrop(leash, destinationBounds.width(), + destinationBounds.height()); + mFinishTransaction.setPosition(leash, destinationBounds.left, + destinationBounds.top); + } } } else { wct = new WindowContainerTransaction(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 7f6118689dad..588b88753eb9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -104,6 +104,7 @@ import com.android.wm.shell.sysui.UserChangeListener; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -215,7 +216,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb private boolean mIsKeyguardShowingOrAnimating; - private Consumer<Boolean> mOnIsInPipStateChangedListener; + private final List<Consumer<Boolean>> mOnIsInPipStateChangedListeners = new ArrayList<>(); @VisibleForTesting interface PipAnimationListener { @@ -501,11 +502,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb false /* saveRestoreSnapFraction */); }); mPipTransitionState.addOnPipTransitionStateChangedListener((oldState, newState) -> { - if (mOnIsInPipStateChangedListener != null) { - final boolean wasInPip = PipTransitionState.isInPip(oldState); - final boolean nowInPip = PipTransitionState.isInPip(newState); - if (nowInPip != wasInPip) { - mOnIsInPipStateChangedListener.accept(nowInPip); + final boolean wasInPip = PipTransitionState.isInPip(oldState); + final boolean nowInPip = PipTransitionState.isInPip(newState); + if (nowInPip != wasInPip) { + for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) { + listener.accept(nowInPip); } } }); @@ -960,13 +961,19 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx); } - private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { - mOnIsInPipStateChangedListener = callback; - if (mOnIsInPipStateChangedListener != null) { + private void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + if (callback != null) { + mOnIsInPipStateChangedListeners.add(callback); callback.accept(mPipTransitionState.isInPip()); } } + private void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + if (callback != null) { + mOnIsInPipStateChangedListeners.remove(callback); + } + } + private void setShelfHeightLocked(boolean visible, int height) { final int shelfHeight = visible ? height : 0; mPipBoundsState.setShelfVisibility(visible, shelfHeight); @@ -1222,9 +1229,16 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { + public void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + mMainExecutor.execute(() -> { + PipController.this.addOnIsInPipStateChangedListener(callback); + }); + } + + @Override + public void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { mMainExecutor.execute(() -> { - PipController.this.setOnIsInPipStateChangedListener(callback); + PipController.this.removeOnIsInPipStateChangedListener(callback); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java index 614ef2ab9831..fcba46108f67 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java @@ -21,6 +21,7 @@ import android.content.Context; import androidx.annotation.NonNull; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; @@ -61,6 +62,7 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { @NonNull PipParamsChangedForwarder pipParamsChangedForwarder, Optional<SplitScreenController> splitScreenOptional, Optional<PipPerfHintController> pipPerfHintControllerOptional, + @NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @NonNull ShellTaskOrganizer shellTaskOrganizer, @@ -68,8 +70,9 @@ public class TvPipTaskOrganizer extends PipTaskOrganizer { super(context, syncTransactionQueue, pipTransitionState, pipBoundsState, pipDisplayLayoutState, boundsHandler, pipMenuController, pipAnimationController, surfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder, - splitScreenOptional, pipPerfHintControllerOptional, displayController, - pipUiEventLogger, shellTaskOrganizer, mainExecutor); + splitScreenOptional, pipPerfHintControllerOptional, Optional.empty(), + rootTaskDisplayAreaOrganizer, displayController, pipUiEventLogger, + shellTaskOrganizer, mainExecutor); mTvPipTransition = tvPipTransition; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index d3f537b8f904..bc0918331168 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -21,6 +21,7 @@ import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.ComponentName; @@ -66,6 +67,8 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; import java.util.function.Consumer; /** @@ -94,7 +97,7 @@ public class PipController implements ConfigurationChangeListener, private final PipTouchHandler mPipTouchHandler; private final ShellExecutor mMainExecutor; private final PipImpl mImpl; - private Consumer<Boolean> mOnIsInPipStateChangedListener; + private final List<Consumer<Boolean>> mOnIsInPipStateChangedListeners = new ArrayList<>(); // Wrapper for making Binder calls into PiP animation listener hosted in launcher's Recents. private PipAnimationListener mPipRecentsAnimationListener; @@ -413,13 +416,13 @@ public class PipController implements ConfigurationChangeListener, if (mPipTransitionState.isInSwipePipToHomeTransition()) { mPipTransitionState.resetSwipePipToHomeState(); } - if (mOnIsInPipStateChangedListener != null) { - mOnIsInPipStateChangedListener.accept(true /* inPip */); + for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) { + listener.accept(true /* inPip */); } break; case PipTransitionState.EXITED_PIP: - if (mOnIsInPipStateChangedListener != null) { - mOnIsInPipStateChangedListener.accept(false /* inPip */); + for (Consumer<Boolean> listener : mOnIsInPipStateChangedListeners) { + listener.accept(false /* inPip */); } break; } @@ -451,13 +454,19 @@ public class PipController implements ConfigurationChangeListener, mPipTransitionState.dump(pw, innerPrefix); } - private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { - mOnIsInPipStateChangedListener = callback; - if (mOnIsInPipStateChangedListener != null) { + private void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + if (callback != null) { + mOnIsInPipStateChangedListeners.add(callback); callback.accept(mPipTransitionState.isInPip()); } } + private void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + if (callback != null) { + mOnIsInPipStateChangedListeners.remove(callback); + } + } + private void setLauncherAppIconSize(int iconSizePx) { mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx); } @@ -473,9 +482,16 @@ public class PipController implements ConfigurationChangeListener, public void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) {} @Override - public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { + public void addOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { + mMainExecutor.execute(() -> { + PipController.this.addOnIsInPipStateChangedListener(callback); + }); + } + + @Override + public void removeOnIsInPipStateChangedListener(@NonNull Consumer<Boolean> callback) { mMainExecutor.execute(() -> { - PipController.this.setOnIsInPipStateChangedListener(callback); + PipController.this.removeOnIsInPipStateChangedListener(callback); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index ea783e9cadb6..3caad0966b1f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; @@ -230,6 +231,11 @@ public class PipTransition extends PipTransitionController implements // If there is no PiP change, exit this transition handler and potentially try others. if (pipChange == null) return false; + // Other targets might have default transforms applied that are not relevant when + // playing PiP transitions, so reset those transforms if needed. + prepareOtherTargetTransforms(info, startTransaction, finishTransaction); + + // Update the PipTransitionState while supplying the PiP leash and token to be cached. Bundle extra = new Bundle(); extra.putParcelable(PIP_TASK_TOKEN, pipChange.getContainer()); extra.putParcelable(PIP_TASK_LEASH, pipChange.getLeash()); @@ -341,17 +347,21 @@ public class PipTransition extends PipTransitionController implements (destinationBounds.height() - overlaySize) / 2f); } - final int startRotation = pipChange.getStartRotation(); - final int endRotation = mPipDisplayLayoutState.getRotation(); - final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 - : startRotation - endRotation; + final int delta = getFixedRotationDelta(info, pipChange); if (delta != ROTATION_0) { - mPipTransitionState.setInFixedRotation(true); - handleBoundsEnterFixedRotation(pipChange, pipActivityChange, endRotation); + // Update transition target changes in place to prepare for fixed rotation. + handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange); } + // Update the src-rect-hint in params in place, to set up initial animator transform. + Rect sourceRectHint = getAdjustedSourceRectHint(info, pipChange, pipActivityChange); + pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint().set(sourceRectHint); + + // Config-at-end transitions need to have their activities transformed before starting + // the animation; this makes the buffer seem like it's been updated to final size. prepareConfigAtEndActivity(startTransaction, finishTransaction, pipChange, pipActivityChange); + startTransaction.merge(finishTransaction); PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash, startTransaction, finishTransaction, destinationBounds, delta); @@ -387,55 +397,36 @@ public class PipTransition extends PipTransitionController implements return false; } + final SurfaceControl pipLeash = getLeash(pipChange); final Rect startBounds = pipChange.getStartAbsBounds(); final Rect endBounds = pipChange.getEndAbsBounds(); - final PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; - final float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params); - final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds, - endBounds); - final Rect adjustedSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint) - : PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio); - - final SurfaceControl pipLeash = mPipTransitionState.getPinnedTaskLeash(); - - // For opening type transitions, if there is a change of mode TO_FRONT/OPEN, - // make sure that change has alpha of 1f, since it's init state might be set to alpha=0f - // by the Transitions framework to simplify Task opening transitions. - if (TransitionUtil.isOpeningType(info.getType())) { - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getLeash() == null) continue; - if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) { - startTransaction.setAlpha(change.getLeash(), 1f); - } - } - } - - final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); - final int startRotation = pipChange.getStartRotation(); - final int endRotation = fixedRotationChange != null - ? fixedRotationChange.getEndFixedRotation() : ROTATION_UNDEFINED; - final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 - : startRotation - endRotation; + final Rect adjustedSourceRectHint = getAdjustedSourceRectHint(info, pipChange, + pipActivityChange); + final int delta = getFixedRotationDelta(info, pipChange); if (delta != ROTATION_0) { - mPipTransitionState.setInFixedRotation(true); - handleBoundsEnterFixedRotation(pipChange, pipActivityChange, - fixedRotationChange.getEndFixedRotation()); + // Update transition target changes in place to prepare for fixed rotation. + handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange); } PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash, startTransaction, finishTransaction, endBounds, delta); - if (sourceRectHint == null) { - // update the src-rect-hint in params in place, to set up initial animator transform. - params.getSourceRectHint().set(adjustedSourceRectHint); + if (PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds, endBounds) == null) { + // If app provided src-rect-hint is invalid, use app icon overlay. animator.setAppIconContentOverlay( mContext, startBounds, endBounds, pipChange.getTaskInfo().topActivityInfo, mPipBoundsState.getLauncherState().getAppIconSizePx()); } + // Update the src-rect-hint in params in place, to set up initial animator transform. + params.getSourceRectHint().set(adjustedSourceRectHint); + + // Config-at-end transitions need to have their activities transformed before starting + // the animation; this makes the buffer seem like it's been updated to final size. prepareConfigAtEndActivity(startTransaction, finishTransaction, pipChange, pipActivityChange); + animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange)); animator.setAnimationEndCallback(() -> { if (animator.getContentOverlayLeash() != null) { @@ -457,11 +448,22 @@ public class PipTransition extends PipTransitionController implements animator.start(); } - private void handleBoundsEnterFixedRotation(TransitionInfo.Change pipTaskChange, - TransitionInfo.Change pipActivityChange, int endRotation) { - final Rect endBounds = pipTaskChange.getEndAbsBounds(); - final Rect endActivityBounds = pipActivityChange.getEndAbsBounds(); - int startRotation = pipTaskChange.getStartRotation(); + private void handleBoundsEnterFixedRotation(TransitionInfo info, + TransitionInfo.Change outPipTaskChange, + TransitionInfo.Change outPipActivityChange) { + final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); + final Rect endBounds = outPipTaskChange.getEndAbsBounds(); + final Rect endActivityBounds = outPipActivityChange.getEndAbsBounds(); + int startRotation = outPipTaskChange.getStartRotation(); + int endRotation = fixedRotationChange != null + ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation(); + + if (startRotation == endRotation) { + return; + } + + // This is used by display change listeners to respond properly to fixed rotation. + mPipTransitionState.setInFixedRotation(true); // Cache the task to activity offset to potentially restore later. Point activityEndOffset = new Point(endActivityBounds.left - endBounds.left, @@ -490,15 +492,15 @@ public class PipTransition extends PipTransitionController implements endBounds.top + activityEndOffset.y); } - private void handleExpandFixedRotation(TransitionInfo.Change pipTaskChange, int endRotation) { - final Rect endBounds = pipTaskChange.getEndAbsBounds(); + private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) { + final Rect endBounds = outPipTaskChange.getEndAbsBounds(); final int width = endBounds.width(); final int height = endBounds.height(); final int left = endBounds.left; final int top = endBounds.top; int newTop, newLeft; - if (endRotation == Surface.ROTATION_90) { + if (delta == Surface.ROTATION_90) { newLeft = top; newTop = -(left + width); } else { @@ -585,15 +587,11 @@ public class PipTransition extends PipTransitionController implements final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds, startBounds); - final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); - final int startRotation = pipChange.getStartRotation(); - final int endRotation = fixedRotationChange != null - ? fixedRotationChange.getEndFixedRotation() : ROTATION_UNDEFINED; - final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 - : endRotation - startRotation; - + // We define delta = startRotation - endRotation, so we need to flip the sign. + final int delta = -getFixedRotationDelta(info, pipChange); if (delta != ROTATION_0) { - handleExpandFixedRotation(pipChange, endRotation); + // Update PiP target change in place to prepare for fixed rotation; + handleExpandFixedRotation(pipChange, delta); } PipExpandAnimator animator = new PipExpandAnimator(mContext, pipLeash, @@ -661,6 +659,72 @@ public class PipTransition extends PipTransitionController implements return null; } + @NonNull + private Rect getAdjustedSourceRectHint(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change pipTaskChange, + @NonNull TransitionInfo.Change pipActivityChange) { + final Rect startBounds = pipTaskChange.getStartAbsBounds(); + final Rect endBounds = pipTaskChange.getEndAbsBounds(); + final PictureInPictureParams params = pipTaskChange.getTaskInfo().pictureInPictureParams; + + // Get the source-rect-hint provided by the app and check its validity; null if invalid. + final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds, + endBounds); + + final Rect adjustedSourceRectHint = new Rect(); + if (sourceRectHint != null) { + adjustedSourceRectHint.set(sourceRectHint); + // If multi-activity PiP, use the parent task before PiP to retrieve display cutouts; + // then, offset the valid app provided source rect hint by the cutout insets. + // For single-activity PiP, just use the pinned task to get the cutouts instead. + TransitionInfo.Change parentBeforePip = pipActivityChange.getLastParent() != null + ? getChangeByToken(info, pipActivityChange.getLastParent()) : null; + Rect cutoutInsets = parentBeforePip != null + ? parentBeforePip.getTaskInfo().displayCutoutInsets + : pipTaskChange.getTaskInfo().displayCutoutInsets; + if (cutoutInsets != null + && getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) { + adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top); + } + } else { + // For non-valid app provided src-rect-hint, calculate one to crop into during + // app icon overlay animation. + float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params); + adjustedSourceRectHint.set( + PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio)); + } + return adjustedSourceRectHint; + } + + @Surface.Rotation + private int getFixedRotationDelta(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change pipChange) { + TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); + int startRotation = pipChange.getStartRotation(); + int endRotation = fixedRotationChange != null + ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation(); + int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 + : startRotation - endRotation; + return delta; + } + + private void prepareOtherTargetTransforms(TransitionInfo info, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction) { + // For opening type transitions, if there is a change of mode TO_FRONT/OPEN, + // make sure that change has alpha of 1f, since it's init state might be set to alpha=0f + // by the Transitions framework to simplify Task opening transitions. + if (TransitionUtil.isOpeningType(info.getType())) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getLeash() == null) continue; + if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) { + startTransaction.setAlpha(change.getLeash(), 1f); + } + } + } + + } + private WindowContainerTransaction getEnterPipTransaction(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { // cache the original task token to check for multi-activity case later diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index cc0e1df115c2..19a73f3631f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -55,7 +55,6 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER; -import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; @@ -1099,16 +1098,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void setSideStagePosition(@SplitPosition int sideStagePosition, @Nullable WindowContainerTransaction wct) { - setSideStagePosition(sideStagePosition, true /* updateBounds */, wct); - } - - private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds, - @Nullable WindowContainerTransaction wct) { if (mSideStagePosition == sideStagePosition) return; mSideStagePosition = sideStagePosition; sendOnStagePositionChanged(); - if (mSideStage.mVisible && updateBounds) { + if (mSideStage.mVisible) { if (wct == null) { // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds. onLayoutSizeChanged(mSplitLayout); @@ -1199,6 +1193,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!isSplitActive()) return; final WindowContainerTransaction wct = new WindowContainerTransaction(); + setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); applyExitSplitScreen(childrenToTop, wct, exitReason); } @@ -1598,6 +1593,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (present) { updateRecentTasksSplitPair(); + } else if (mMainStage.getChildCount() == 0 && mSideStage.getChildCount() == 0) { + mRecentTasks.ifPresent(recentTasks -> { + // remove the split pair mapping from recentTasks, and disable further updates + // to splits in the recents until we enter split again. + recentTasks.removeSplitPair(taskId); + }); + exitSplitScreen(mMainStage, EXIT_REASON_ROOT_TASK_VANISHED); } for (int i = mListeners.size() - 1; i >= 0; --i) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java index d38b848fbb4d..329a10998f23 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleViewTest.java @@ -16,9 +16,8 @@ package com.android.wm.shell.bubbles.bar; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; -import android.graphics.drawable.ColorDrawable; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -47,10 +46,9 @@ public class BubbleBarHandleViewTest extends ShellTestCase { public void testUpdateHandleColor_lightBg() { mHandleView.updateHandleColor(false /* isRegionDark */, false /* animated */); - assertTrue(mHandleView.getClipToOutline()); - assertTrue(mHandleView.getBackground() instanceof ColorDrawable); - ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground(); - assertEquals(bgDrawable.getColor(), + assertFalse(mHandleView.getClipToOutline()); + int handleColor = mHandleView.mHandlePaint.getColor(); + assertEquals(handleColor, ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_dark)); } @@ -58,10 +56,9 @@ public class BubbleBarHandleViewTest extends ShellTestCase { public void testUpdateHandleColor_darkBg() { mHandleView.updateHandleColor(true /* isRegionDark */, false /* animated */); - assertTrue(mHandleView.getClipToOutline()); - assertTrue(mHandleView.getBackground() instanceof ColorDrawable); - ColorDrawable bgDrawable = (ColorDrawable) mHandleView.getBackground(); - assertEquals(bgDrawable.getColor(), + assertFalse(mHandleView.getClipToOutline()); + int handleColor = mHandleView.mHandlePaint.getColor(); + assertEquals(handleColor, ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_handle_light)); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipAppOpsListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipAppOpsListenerTest.java new file mode 100644 index 000000000000..b9490b881d08 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipAppOpsListenerTest.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2024 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.wm.shell.common.pip; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.app.AppOpsManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Pair; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.ShellExecutor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit test against {@link PipAppOpsListener}. + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipAppOpsListenerTest { + + @Mock private Context mMockContext; + @Mock private PackageManager mMockPackageManager; + @Mock private AppOpsManager mMockAppOpsManager; + @Mock private PipAppOpsListener.Callback mMockCallback; + @Mock private ShellExecutor mMockExecutor; + + private PipAppOpsListener mPipAppOpsListener; + + private ArgumentCaptor<AppOpsManager.OnOpChangedListener> mOnOpChangedListenerCaptor; + private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; + private Pair<ComponentName, Integer> mTopPipActivity; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); + when(mMockContext.getSystemService(Context.APP_OPS_SERVICE)) + .thenReturn(mMockAppOpsManager); + mOnOpChangedListenerCaptor = ArgumentCaptor.forClass( + AppOpsManager.OnOpChangedListener.class); + mRunnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + } + + @Test + public void onActivityPinned_registerAppOpsListener() { + String packageName = "com.android.test.pip"; + mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor); + + mPipAppOpsListener.onActivityPinned(packageName); + + verify(mMockAppOpsManager).startWatchingMode( + eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName), + any(AppOpsManager.OnOpChangedListener.class)); + } + + @Test + public void onActivityUnpinned_unregisterAppOpsListener() { + mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor); + + mPipAppOpsListener.onActivityUnpinned(); + + verify(mMockAppOpsManager).stopWatchingMode(any(AppOpsManager.OnOpChangedListener.class)); + } + + @Test + public void disablePipAppOps_dismissPip() throws PackageManager.NameNotFoundException { + String packageName = "com.android.test.pip"; + mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor); + // Set up the top pip activity info as mTopPipActivity + mTopPipActivity = new Pair<>(new ComponentName(packageName, "PipActivity"), 0); + mPipAppOpsListener.setTopPipActivityInfoSupplier(this::getTopPipActivity); + // Set up the application info as mApplicationInfo + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = packageName; + when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); + // Mock the mode to be **not** allowed + when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), eq(packageName))) + .thenReturn(AppOpsManager.MODE_DEFAULT); + // Set up the initial state + mPipAppOpsListener.onActivityPinned(packageName); + verify(mMockAppOpsManager).startWatchingMode( + eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName), + mOnOpChangedListenerCaptor.capture()); + AppOpsManager.OnOpChangedListener opChangedListener = mOnOpChangedListenerCaptor.getValue(); + + opChangedListener.onOpChanged(String.valueOf(AppOpsManager.OP_PICTURE_IN_PICTURE), + packageName); + + verify(mMockExecutor).execute(mRunnableArgumentCaptor.capture()); + Runnable runnable = mRunnableArgumentCaptor.getValue(); + runnable.run(); + verify(mMockCallback).dismissPip(); + } + + @Test + public void disablePipAppOps_differentPackage_doNothing() + throws PackageManager.NameNotFoundException { + String packageName = "com.android.test.pip"; + mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor); + // Set up the top pip activity info as mTopPipActivity + mTopPipActivity = new Pair<>(new ComponentName(packageName, "PipActivity"), 0); + mPipAppOpsListener.setTopPipActivityInfoSupplier(this::getTopPipActivity); + // Set up the application info as mApplicationInfo + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = packageName + ".modified"; + when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); + // Mock the mode to be **not** allowed + when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), eq(packageName))) + .thenReturn(AppOpsManager.MODE_DEFAULT); + // Set up the initial state + mPipAppOpsListener.onActivityPinned(packageName); + verify(mMockAppOpsManager).startWatchingMode( + eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName), + mOnOpChangedListenerCaptor.capture()); + AppOpsManager.OnOpChangedListener opChangedListener = mOnOpChangedListenerCaptor.getValue(); + + opChangedListener.onOpChanged(String.valueOf(AppOpsManager.OP_PICTURE_IN_PICTURE), + packageName); + + verifyZeroInteractions(mMockExecutor); + } + + @Test + public void disablePipAppOps_nameNotFound_unregisterAppOpsListener() + throws PackageManager.NameNotFoundException { + String packageName = "com.android.test.pip"; + mPipAppOpsListener = new PipAppOpsListener(mMockContext, mMockCallback, mMockExecutor); + // Set up the top pip activity info as mTopPipActivity + mTopPipActivity = new Pair<>(new ComponentName(packageName, "PipActivity"), 0); + mPipAppOpsListener.setTopPipActivityInfoSupplier(this::getTopPipActivity); + // Set up the application info as mApplicationInfo + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.packageName = packageName; + when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenThrow(PackageManager.NameNotFoundException.class); + // Mock the mode to be **not** allowed + when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), eq(packageName))) + .thenReturn(AppOpsManager.MODE_DEFAULT); + // Set up the initial state + mPipAppOpsListener.onActivityPinned(packageName); + verify(mMockAppOpsManager).startWatchingMode( + eq(AppOpsManager.OP_PICTURE_IN_PICTURE), eq(packageName), + mOnOpChangedListenerCaptor.capture()); + AppOpsManager.OnOpChangedListener opChangedListener = mOnOpChangedListenerCaptor.getValue(); + + opChangedListener.onOpChanged(String.valueOf(AppOpsManager.OP_PICTURE_IN_PICTURE), + packageName); + + verify(mMockAppOpsManager).stopWatchingMode(any(AppOpsManager.OnOpChangedListener.class)); + } + + private Pair<ComponentName, Integer> getTopPipActivity(Context context) { + return mTopPipActivity; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 414c1a658b95..7f790d574a7e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -936,6 +936,28 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test + fun saveBoundsBeforeMinimize_boundsSavedByTaskId() { + val taskId = 1 + val bounds = Rect(0, 0, 200, 200) + + repo.saveBoundsBeforeMinimize(taskId, bounds) + + assertThat(repo.removeBoundsBeforeMinimize(taskId)).isEqualTo(bounds) + } + + @Test + fun removeBoundsBeforeMinimize_returnsNullAfterBoundsRemoved() { + val taskId = 1 + val bounds = Rect(0, 0, 200, 200) + repo.saveBoundsBeforeMinimize(taskId, bounds) + repo.removeBoundsBeforeMinimize(taskId) + + val boundsBeforeMinimize = repo.removeBoundsBeforeMinimize(taskId) + + assertThat(boundsBeforeMinimize).isNull() + } + + @Test fun getExpandedTasksOrdered_returnsFreeformTasksInCorrectOrder() { repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 3, isVisible = true) repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 01b69aed8465..456b50da095b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo +import android.graphics.Rect import android.os.Binder import android.os.Handler import android.platform.test.annotations.DisableFlags @@ -24,8 +25,10 @@ import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY +import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.TransitionInfo import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER @@ -63,6 +66,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.any +import org.mockito.Mockito.mock import org.mockito.Mockito.spy import org.mockito.Mockito.`when` import org.mockito.kotlin.eq @@ -235,6 +239,30 @@ class DesktopTasksLimiterTest : ShellTestCase() { } @Test + fun onTransitionReady_pendingTransition_changeTaskToBack_boundsSaved() { + val bounds = Rect(0, 0, 200, 200) + val transition = Binder() + val task = setUpFreeformTask() + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + val change = TransitionInfo.Change(task.token, mock(SurfaceControl::class.java)).apply { + mode = TRANSIT_TO_BACK + taskInfo = task + setStartAbsBounds(bounds) + } + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfo(TRANSIT_OPEN, TransitionInfo.FLAG_NONE).apply { addChange(change) }, + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() + assertThat(desktopTaskRepo.removeBoundsBeforeMinimize(taskId = task.taskId)).isEqualTo( + bounds) + } + + @Test fun onTransitionReady_transitionMergedFromPending_taskIsMinimized() { val mergedTransition = Binder() val newTransition = Binder() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index bcb7461bfae7..5f58265b45f5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -47,6 +47,7 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; import com.android.wm.shell.MockSurfaceControlHelper; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; @@ -61,6 +62,7 @@ import com.android.wm.shell.common.pip.PipKeepClearAlgorithmInterface; import com.android.wm.shell.common.pip.PipSnapAlgorithm; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.SizeSpecSource; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -90,6 +92,8 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper; @Mock private PipUiEventLogger mMockPipUiEventLogger; @Mock private Optional<SplitScreenController> mMockOptionalSplitScreen; + @Mock private Optional<DesktopRepository> mMockOptionalDesktopRepository; + @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; @Mock private PipParamsChangedForwarder mMockPipParamsChangedForwarder; private TestShellExecutor mMainExecutor; @@ -120,8 +124,10 @@ public class PipTaskOrganizerTest extends ShellTestCase { mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController, mMockPipSurfaceTransactionHelper, mMockPipTransitionController, mMockPipParamsChangedForwarder, mMockOptionalSplitScreen, - Optional.empty() /* pipPerfHintControllerOptional */, mMockDisplayController, - mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor); + Optional.empty() /* pipPerfHintControllerOptional */, + mMockOptionalDesktopRepository, mRootTaskDisplayAreaOrganizer, + mMockDisplayController, mMockPipUiEventLogger, mMockShellTaskOrganizer, + mMainExecutor); mMainExecutor.flushAll(); preparePipTaskOrg(); preparePipSurfaceTransactionHelper(); diff --git a/libs/appfunctions/Android.bp b/libs/appfunctions/Android.bp index c6cee07d1946..5ab5a7a59c2a 100644 --- a/libs/appfunctions/Android.bp +++ b/libs/appfunctions/Android.bp @@ -18,10 +18,10 @@ package { } java_sdk_library { - name: "com.google.android.appfunctions.sidecar", + name: "com.android.extensions.appfunctions", owner: "google", srcs: ["java/**/*.java"], - api_packages: ["com.google.android.appfunctions.sidecar"], + api_packages: ["com.android.extensions.appfunctions"], dex_preopt: { enabled: false, }, @@ -31,9 +31,9 @@ java_sdk_library { } prebuilt_etc { - name: "appfunctions.sidecar.xml", + name: "appfunctions.extension.xml", system_ext_specific: true, sub_dir: "permissions", - src: "appfunctions.sidecar.xml", + src: "appfunctions.extension.xml", filename_from_src: true, } diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index faf84a8ab5ac..de402095e195 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -1,9 +1,29 @@ // Signature format: 2.0 -package com.google.android.appfunctions.sidecar { +package com.android.extensions.appfunctions { + + public final class AppFunctionException extends java.lang.Exception { + ctor public AppFunctionException(int, @Nullable String); + ctor public AppFunctionException(int, @Nullable String, @NonNull android.os.Bundle); + method public int getErrorCategory(); + method public int getErrorCode(); + method @Nullable public String getErrorMessage(); + method @NonNull public android.os.Bundle getExtras(); + field public static final int ERROR_APP_UNKNOWN_ERROR = 3000; // 0xbb8 + field public static final int ERROR_CANCELLED = 2001; // 0x7d1 + field public static final int ERROR_CATEGORY_APP = 3; // 0x3 + field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1 + field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2 + field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0 + field public static final int ERROR_DENIED = 1000; // 0x3e8 + field public static final int ERROR_DISABLED = 1002; // 0x3ea + field public static final int ERROR_FUNCTION_NOT_FOUND = 1003; // 0x3eb + field public static final int ERROR_INVALID_ARGUMENT = 1001; // 0x3e9 + field public static final int ERROR_SYSTEM_ERROR = 2000; // 0x7d0 + } public final class AppFunctionManager { ctor public AppFunctionManager(android.content.Context); - method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void isAppFunctionEnabled(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); @@ -15,7 +35,7 @@ package com.google.android.appfunctions.sidecar { public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @MainThread public abstract void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } @@ -29,33 +49,17 @@ package com.google.android.appfunctions.sidecar { public static final class ExecuteAppFunctionRequest.Builder { ctor public ExecuteAppFunctionRequest.Builder(@NonNull String, @NonNull String); - method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest build(); - method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle); - method @NonNull public com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument); + method @NonNull public com.android.extensions.appfunctions.ExecuteAppFunctionRequest build(); + method @NonNull public com.android.extensions.appfunctions.ExecuteAppFunctionRequest.Builder setExtras(@NonNull android.os.Bundle); + method @NonNull public com.android.extensions.appfunctions.ExecuteAppFunctionRequest.Builder setParameters(@NonNull android.app.appsearch.GenericDocument); } public final class ExecuteAppFunctionResponse { - method public int getErrorCategory(); - method @Nullable public String getErrorMessage(); + ctor public ExecuteAppFunctionResponse(@NonNull android.app.appsearch.GenericDocument); + ctor public ExecuteAppFunctionResponse(@NonNull android.app.appsearch.GenericDocument, @NonNull android.os.Bundle); method @NonNull public android.os.Bundle getExtras(); - method public int getResultCode(); method @NonNull public android.app.appsearch.GenericDocument getResultDocument(); - method public boolean isSuccess(); - method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle); - method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle); - field public static final int ERROR_CATEGORY_APP = 3; // 0x3 - field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1 - field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2 - field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0 - field public static final String PROPERTY_RETURN_VALUE = "returnValue"; - field public static final int RESULT_APP_UNKNOWN_ERROR = 3000; // 0xbb8 - field public static final int RESULT_CANCELLED = 2001; // 0x7d1 - field public static final int RESULT_DENIED = 1000; // 0x3e8 - field public static final int RESULT_DISABLED = 1002; // 0x3ea - field public static final int RESULT_FUNCTION_NOT_FOUND = 1003; // 0x3eb - field public static final int RESULT_INVALID_ARGUMENT = 1001; // 0x3e9 - field public static final int RESULT_OK = 0; // 0x0 - field public static final int RESULT_SYSTEM_ERROR = 2000; // 0x7d0 + field public static final String PROPERTY_RETURN_VALUE = "androidAppfunctionsReturnValue"; } } diff --git a/libs/appfunctions/appfunctions.sidecar.xml b/libs/appfunctions/appfunctions.extension.xml index bef8b6ec7ce6..dd09cc39d12f 100644 --- a/libs/appfunctions/appfunctions.sidecar.xml +++ b/libs/appfunctions/appfunctions.extension.xml @@ -16,6 +16,6 @@ --> <permissions> <library - name="com.google.android.appfunctions.sidecar" - file="/system_ext/framework/com.google.android.appfunctions.sidecar.jar"/> + name="com.android.extensions.appfunctions" + file="/system_ext/framework/com.android.extensions.appfunctions.jar"/> </permissions>
\ No newline at end of file diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java new file mode 100644 index 000000000000..28c3b3df9b1c --- /dev/null +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionException.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2024 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.extensions.appfunctions; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Represents an app function related errors. */ +public final class AppFunctionException extends Exception { + /** + * The caller does not have the permission to execute an app function. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_DENIED = 1000; + + /** + * The caller supplied invalid arguments to the execution request. + * + * <p>This error may be considered similar to {@link IllegalArgumentException}. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_INVALID_ARGUMENT = 1001; + + /** + * The caller tried to execute a disabled app function. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_DISABLED = 1002; + + /** + * The caller tried to execute a function that does not exist. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int ERROR_FUNCTION_NOT_FOUND = 1003; + + /** + * An internal unexpected error coming from the system. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int ERROR_SYSTEM_ERROR = 2000; + + /** + * The operation was cancelled. Use this error code to report that a cancellation is done after + * receiving a cancellation signal. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int ERROR_CANCELLED = 2001; + + /** + * An unknown error occurred while processing the call in the AppFunctionService. + * + * <p>This error is thrown when the service is connected in the remote application but an + * unexpected error is thrown from the bound application. + * + * <p>This error is in the {@link #ERROR_CATEGORY_APP} category. + */ + public static final int ERROR_APP_UNKNOWN_ERROR = 3000; + + /** + * The error category is unknown. + * + * <p>This is the default value for {@link #getErrorCategory}. + */ + public static final int ERROR_CATEGORY_UNKNOWN = 0; + + /** + * The error is caused by the app requesting a function execution. + * + * <p>For example, the caller provided invalid parameters in the execution request e.g. an + * invalid function ID. + * + * <p>Errors in the category fall in the range 1000-1999 inclusive. + */ + public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; + + /** + * The error is caused by an issue in the system. + * + * <p>For example, the AppFunctionService implementation is not found by the system. + * + * <p>Errors in the category fall in the range 2000-2999 inclusive. + */ + public static final int ERROR_CATEGORY_SYSTEM = 2; + + /** + * The error is caused by the app providing the function. + * + * <p>For example, the app crashed when the system is executing the request. + * + * <p>Errors in the category fall in the range 3000-3999 inclusive. + */ + public static final int ERROR_CATEGORY_APP = 3; + + private final int mErrorCode; + @Nullable private final String mErrorMessage; + @NonNull private final Bundle mExtras; + + public AppFunctionException(int errorCode, @Nullable String errorMessage) { + this(errorCode, errorMessage, Bundle.EMPTY); + } + + public AppFunctionException( + int errorCode, @Nullable String errorMessage, @NonNull Bundle extras) { + mErrorCode = errorCode; + mErrorMessage = errorMessage; + mExtras = extras; + } + + /** Returns one of the {@code ERROR} constants. */ + @ErrorCode + public int getErrorCode() { + return mErrorCode; + } + + /** Returns the error message. */ + @Nullable + public String getErrorMessage() { + return mErrorMessage; + } + + /** + * Returns the error category. + * + * <p>This method categorizes errors based on their underlying cause, allowing developers to + * implement targeted error handling and provide more informative error messages to users. It + * maps ranges of error codes to specific error categories. + * + * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the error code does not belong to + * any error category. + * + * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding + * error code ranges. + */ + @ErrorCategory + public int getErrorCategory() { + if (mErrorCode >= 1000 && mErrorCode < 2000) { + return ERROR_CATEGORY_REQUEST_ERROR; + } + if (mErrorCode >= 2000 && mErrorCode < 3000) { + return ERROR_CATEGORY_SYSTEM; + } + if (mErrorCode >= 3000 && mErrorCode < 4000) { + return ERROR_CATEGORY_APP; + } + return ERROR_CATEGORY_UNKNOWN; + } + + @NonNull + public Bundle getExtras() { + return mExtras; + } + + /** + * Error codes. + * + * @hide + */ + @IntDef( + prefix = {"ERROR_"}, + value = { + ERROR_DENIED, + ERROR_APP_UNKNOWN_ERROR, + ERROR_FUNCTION_NOT_FOUND, + ERROR_SYSTEM_ERROR, + ERROR_INVALID_ARGUMENT, + ERROR_DISABLED, + ERROR_CANCELLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCode {} + + /** + * Error categories. + * + * @hide + */ + @IntDef( + prefix = {"ERROR_CATEGORY_"}, + value = { + ERROR_CATEGORY_UNKNOWN, + ERROR_CATEGORY_REQUEST_ERROR, + ERROR_CATEGORY_APP, + ERROR_CATEGORY_SYSTEM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCategory {} +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java index 2075104ff868..9eb66a33fedc 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.android.appfunctions.sidecar; +package com.android.extensions.appfunctions; import android.Manifest; import android.annotation.CallbackExecutor; @@ -31,7 +31,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.concurrent.Executor; -import java.util.function.Consumer; /** * Provides app functions related functionalities. @@ -115,7 +114,9 @@ public final class AppFunctionManager { @NonNull ExecuteAppFunctionRequest sidecarRequest, @NonNull @CallbackExecutor Executor executor, @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { + @NonNull + OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> + callback) { Objects.requireNonNull(sidecarRequest); Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -126,10 +127,20 @@ public final class AppFunctionManager { platformRequest, executor, cancellationSignal, - (platformResponse) -> { - callback.accept( - SidecarConverter.getSidecarExecuteAppFunctionResponse( - platformResponse)); + new OutcomeReceiver<>() { + @Override + public void onResult( + android.app.appfunctions.ExecuteAppFunctionResponse result) { + callback.onResult( + SidecarConverter.getSidecarExecuteAppFunctionResponse(result)); + } + + @Override + public void onError( + android.app.appfunctions.AppFunctionException exception) { + callback.onError( + SidecarConverter.getSidecarAppFunctionException(exception)); + } }); } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java index 0dc87e45b7e3..55f579138218 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.google.android.appfunctions.sidecar; +package com.android.extensions.appfunctions; -import static android.Manifest.permission.BIND_APP_FUNCTION_SERVICE; +import static com.android.extensions.appfunctions.SidecarConverter.getPlatformAppFunctionException; +import static com.android.extensions.appfunctions.SidecarConverter.getPlatformExecuteAppFunctionResponse; import android.annotation.MainThread; import android.annotation.NonNull; @@ -26,9 +27,7 @@ import android.content.Intent; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; -import android.util.Log; - -import java.util.function.Consumer; +import android.os.OutcomeReceiver; /** * Abstract base class to provide app functions to the system. @@ -80,10 +79,18 @@ public abstract class AppFunctionService extends Service { platformRequest), callingPackage, cancellationSignal, - (sidecarResponse) -> { - callback.accept( - SidecarConverter.getPlatformExecuteAppFunctionResponse( - sidecarResponse)); + new OutcomeReceiver<>() { + @Override + public void onResult(ExecuteAppFunctionResponse result) { + callback.onResult( + getPlatformExecuteAppFunctionResponse(result)); + } + + @Override + public void onError(AppFunctionException exception) { + callback.onError( + getPlatformAppFunctionException(exception)); + } }); }); @@ -116,12 +123,14 @@ public abstract class AppFunctionService extends Service { * @param request The function execution request. * @param callingPackage The package name of the app that is requesting the execution. * @param cancellationSignal A signal to cancel the execution. - * @param callback A callback to report back the result. + * @param callback A callback to report back the result or error. */ @MainThread public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback); + @NonNull + OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> + callback); } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionRequest.java index 593c5213dd52..baddc245f0f1 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.android.appfunctions.sidecar; +package com.android.extensions.appfunctions; import android.annotation.NonNull; import android.app.appsearch.GenericDocument; @@ -91,8 +91,8 @@ public final class ExecuteAppFunctionRequest { * Returns the function parameters. The key is the parameter name, and the value is the * parameter value. * - * <p>The bundle may have missing parameters. Developers are advised to implement defensive - * handling measures. + * <p>The {@link GenericDocument} may have missing parameters. Developers are advised to + * implement defensive handling measures. * * <p>Similar to {@link #getFunctionIdentifier()} the parameters required by a function can be * obtained by querying AppSearch for the corresponding {@code AppFunctionStaticMetadata}. This diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java new file mode 100644 index 000000000000..0826f04a50dd --- /dev/null +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/ExecuteAppFunctionResponse.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 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.extensions.appfunctions; + +import android.annotation.NonNull; +import android.app.appfunctions.AppFunctionManager; +import android.app.appsearch.GenericDocument; +import android.os.Bundle; + +import java.util.Objects; + +/** The response to an app function execution. */ +public final class ExecuteAppFunctionResponse { + /** + * The name of the property that stores the function return value within the {@code + * resultDocument}. + * + * <p>See {@link GenericDocument#getProperty(String)} for more information. + * + * <p>If the function returns {@code void} or throws an error, the {@code resultDocument} will + * be empty {@link GenericDocument}. + * + * <p>If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will + * return {@code null}. + * + * <p>See {@link #getResultDocument} for more information on extracting the return value. + */ + public static final String PROPERTY_RETURN_VALUE = "androidAppfunctionsReturnValue"; + + /** + * Returns the return value of the executed function. + * + * <p>The return value is stored in a {@link GenericDocument} with the key {@link + * #PROPERTY_RETURN_VALUE}. + * + * <p>See {@link #getResultDocument} for more information on extracting the return value. + */ + @NonNull private final GenericDocument mResultDocument; + + /** Returns the additional metadata data relevant to this function execution response. */ + @NonNull private final Bundle mExtras; + + /** + * @param resultDocument The return value of the executed function. + */ + public ExecuteAppFunctionResponse(@NonNull GenericDocument resultDocument) { + this(resultDocument, Bundle.EMPTY); + } + + /** + * @param resultDocument The return value of the executed function. + * @param extras The additional metadata for this function execution response. + */ + public ExecuteAppFunctionResponse( + @NonNull GenericDocument resultDocument, @NonNull Bundle extras) { + mResultDocument = Objects.requireNonNull(resultDocument); + mExtras = Objects.requireNonNull(extras); + } + + /** + * Returns a generic document containing the return value of the executed function. + * + * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. + * + * <p>Sample code for extracting the return value: + * + * <pre> + * GenericDocument resultDocument = response.getResultDocument(); + * Object returnValue = resultDocument.getProperty(PROPERTY_RETURN_VALUE); + * if (returnValue != null) { + * // Cast returnValue to expected type, or use {@link GenericDocument#getPropertyString}, + * // {@link GenericDocument#getPropertyLong} etc. + * // Do something with the returnValue + * } + * </pre> + * + * @see AppFunctionManager on how to determine the expected function return. + */ + @NonNull + public GenericDocument getResultDocument() { + return mResultDocument; + } + + /** Returns the additional metadata for this function execution response. */ + @NonNull + public Bundle getExtras() { + return mExtras; + } +} diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java b/libs/appfunctions/java/com/android/extensions/appfunctions/SidecarConverter.java index b1b05f79f33f..5e1fc7e684e2 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/SidecarConverter.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/SidecarConverter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.android.appfunctions.sidecar; +package com.android.extensions.appfunctions; import android.annotation.NonNull; @@ -28,46 +28,50 @@ public final class SidecarConverter { private SidecarConverter() {} /** - * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest} - * into platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest} + * Converts sidecar's {@link ExecuteAppFunctionRequest} into platform's {@link + * android.app.appfunctions.ExecuteAppFunctionRequest} * * @hide */ @NonNull public static android.app.appfunctions.ExecuteAppFunctionRequest getPlatformExecuteAppFunctionRequest(@NonNull ExecuteAppFunctionRequest request) { - return new - android.app.appfunctions.ExecuteAppFunctionRequest.Builder( - request.getTargetPackageName(), - request.getFunctionIdentifier()) + return new android.app.appfunctions.ExecuteAppFunctionRequest.Builder( + request.getTargetPackageName(), request.getFunctionIdentifier()) .setExtras(request.getExtras()) .setParameters(request.getParameters()) .build(); } /** - * Converts sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse} - * into platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse} + * Converts sidecar's {@link ExecuteAppFunctionResponse} into platform's {@link + * android.app.appfunctions.ExecuteAppFunctionResponse} * * @hide */ @NonNull public static android.app.appfunctions.ExecuteAppFunctionResponse getPlatformExecuteAppFunctionResponse(@NonNull ExecuteAppFunctionResponse response) { - if (response.isSuccess()) { - return android.app.appfunctions.ExecuteAppFunctionResponse.newSuccess( - response.getResultDocument(), response.getExtras()); - } else { - return android.app.appfunctions.ExecuteAppFunctionResponse.newFailure( - response.getResultCode(), - response.getErrorMessage(), - response.getExtras()); - } + return new android.app.appfunctions.ExecuteAppFunctionResponse( + response.getResultDocument(), response.getExtras()); } /** - * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest} - * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest} + * Converts sidecar's {@link AppFunctionException} into platform's {@link + * android.app.appfunctions.AppFunctionException} + * + * @hide + */ + @NonNull + public static android.app.appfunctions.AppFunctionException + getPlatformAppFunctionException(@NonNull AppFunctionException exception) { + return new android.app.appfunctions.AppFunctionException( + exception.getErrorCode(), exception.getErrorMessage(), exception.getExtras()); + } + + /** + * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionRequest} into sidecar's + * {@link ExecuteAppFunctionRequest} * * @hide */ @@ -75,30 +79,34 @@ public final class SidecarConverter { public static ExecuteAppFunctionRequest getSidecarExecuteAppFunctionRequest( @NonNull android.app.appfunctions.ExecuteAppFunctionRequest request) { return new ExecuteAppFunctionRequest.Builder( - request.getTargetPackageName(), - request.getFunctionIdentifier()) + request.getTargetPackageName(), request.getFunctionIdentifier()) .setExtras(request.getExtras()) .setParameters(request.getParameters()) .build(); } /** - * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse} - * into sidecar's {@link com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse} + * Converts platform's {@link android.app.appfunctions.ExecuteAppFunctionResponse} into + * sidecar's {@link ExecuteAppFunctionResponse} * * @hide */ @NonNull public static ExecuteAppFunctionResponse getSidecarExecuteAppFunctionResponse( @NonNull android.app.appfunctions.ExecuteAppFunctionResponse response) { - if (response.isSuccess()) { - return ExecuteAppFunctionResponse.newSuccess( - response.getResultDocument(), response.getExtras()); - } else { - return ExecuteAppFunctionResponse.newFailure( - response.getResultCode(), - response.getErrorMessage(), - response.getExtras()); - } + return new ExecuteAppFunctionResponse(response.getResultDocument(), response.getExtras()); + } + + /** + * Converts platform's {@link android.app.appfunctions.AppFunctionException} into + * sidecar's {@link AppFunctionException} + * + * @hide + */ + @NonNull + public static AppFunctionException getSidecarAppFunctionException( + @NonNull android.app.appfunctions.AppFunctionException exception) { + return new AppFunctionException( + exception.getErrorCode(), exception.getErrorMessage(), exception.getExtras()); } } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java deleted file mode 100644 index 4e88fb025a9d..000000000000 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright (C) 2024 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.google.android.appfunctions.sidecar; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.appsearch.GenericDocument; -import android.os.Bundle; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Objects; - -/** - * The response to an app function execution. - * - * <p>This class copies {@link android.app.appfunctions.ExecuteAppFunctionResponse} without parcel - * functionality and exposes it here as a sidecar library (avoiding direct dependency on the - * platform API). - */ -public final class ExecuteAppFunctionResponse { - /** - * The name of the property that stores the function return value within the {@code - * resultDocument}. - * - * <p>See {@link GenericDocument#getProperty(String)} for more information. - * - * <p>If the function returns {@code void} or throws an error, the {@code resultDocument} will - * be empty {@link GenericDocument}. - * - * <p>If the {@code resultDocument} is empty, {@link GenericDocument#getProperty(String)} will - * return {@code null}. - * - * <p>See {@link #getResultDocument} for more information on extracting the return value. - */ - public static final String PROPERTY_RETURN_VALUE = "returnValue"; - - /** - * The call was successful. - * - * <p>This result code does not belong in an error category. - */ - public static final int RESULT_OK = 0; - - /** - * The caller does not have the permission to execute an app function. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_DENIED = 1000; - - /** - * The caller supplied invalid arguments to the execution request. - * - * <p>This error may be considered similar to {@link IllegalArgumentException}. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_INVALID_ARGUMENT = 1001; - - /** - * The caller tried to execute a disabled app function. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_DISABLED = 1002; - - /** - * The caller tried to execute a function that does not exist. - * - * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. - */ - public static final int RESULT_FUNCTION_NOT_FOUND = 1003; - - /** - * An internal unexpected error coming from the system. - * - * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. - */ - public static final int RESULT_SYSTEM_ERROR = 2000; - - /** - * The operation was cancelled. Use this error code to report that a cancellation is done after - * receiving a cancellation signal. - * - * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. - */ - public static final int RESULT_CANCELLED = 2001; - - /** - * An unknown error occurred while processing the call in the AppFunctionService. - * - * <p>This error is thrown when the service is connected in the remote application but an - * unexpected error is thrown from the bound application. - * - * <p>This error is in the {@link #ERROR_CATEGORY_APP} category. - */ - public static final int RESULT_APP_UNKNOWN_ERROR = 3000; - - /** - * The error category is unknown. - * - * <p>This is the default value for {@link #getErrorCategory}. - */ - public static final int ERROR_CATEGORY_UNKNOWN = 0; - - /** - * The error is caused by the app requesting a function execution. - * - * <p>For example, the caller provided invalid parameters in the execution request e.g. an - * invalid function ID. - * - * <p>Errors in the category fall in the range 1000-1999 inclusive. - */ - public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; - - /** - * The error is caused by an issue in the system. - * - * <p>For example, the AppFunctionService implementation is not found by the system. - * - * <p>Errors in the category fall in the range 2000-2999 inclusive. - */ - public static final int ERROR_CATEGORY_SYSTEM = 2; - - /** - * The error is caused by the app providing the function. - * - * <p>For example, the app crashed when the system is executing the request. - * - * <p>Errors in the category fall in the range 3000-3999 inclusive. - */ - public static final int ERROR_CATEGORY_APP = 3; - - /** The result code of the app function execution. */ - @ResultCode private final int mResultCode; - - /** - * The error message associated with the result, if any. This is {@code null} if the result code - * is {@link #RESULT_OK}. - */ - @Nullable private final String mErrorMessage; - - /** - * Returns the return value of the executed function. - * - * <p>The return value is stored in a {@link GenericDocument} with the key {@link - * #PROPERTY_RETURN_VALUE}. - * - * <p>See {@link #getResultDocument} for more information on extracting the return value. - */ - @NonNull private final GenericDocument mResultDocument; - - /** Returns the additional metadata data relevant to this function execution response. */ - @NonNull private final Bundle mExtras; - - private ExecuteAppFunctionResponse( - @NonNull GenericDocument resultDocument, - @NonNull Bundle extras, - @ResultCode int resultCode, - @Nullable String errorMessage) { - mResultDocument = Objects.requireNonNull(resultDocument); - mExtras = Objects.requireNonNull(extras); - mResultCode = resultCode; - mErrorMessage = errorMessage; - } - - /** - * Returns result codes from throwable. - * - * @hide - */ - static @ResultCode int getResultCode(@NonNull Throwable t) { - if (t instanceof IllegalArgumentException) { - return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT; - } - return ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR; - } - - /** - * Returns a successful response. - * - * @param resultDocument The return value of the executed function. - * @param extras The additional metadata data relevant to this function execution response. - */ - @NonNull - public static ExecuteAppFunctionResponse newSuccess( - @NonNull GenericDocument resultDocument, @Nullable Bundle extras) { - Objects.requireNonNull(resultDocument); - Bundle actualExtras = getActualExtras(extras); - - return new ExecuteAppFunctionResponse( - resultDocument, actualExtras, RESULT_OK, /* errorMessage= */ null); - } - - /** - * Returns a failure response. - * - * @param resultCode The result code of the app function execution. - * @param extras The additional metadata data relevant to this function execution response. - * @param errorMessage The error message associated with the result, if any. - */ - @NonNull - public static ExecuteAppFunctionResponse newFailure( - @ResultCode int resultCode, @Nullable String errorMessage, @Nullable Bundle extras) { - if (resultCode == RESULT_OK) { - throw new IllegalArgumentException("resultCode must not be RESULT_OK"); - } - Bundle actualExtras = getActualExtras(extras); - GenericDocument emptyDocument = new GenericDocument.Builder<>("", "", "").build(); - return new ExecuteAppFunctionResponse( - emptyDocument, actualExtras, resultCode, errorMessage); - } - - private static Bundle getActualExtras(@Nullable Bundle extras) { - if (extras == null) { - return Bundle.EMPTY; - } - return extras; - } - - /** - * Returns the error category of the {@link ExecuteAppFunctionResponse}. - * - * <p>This method categorizes errors based on their underlying cause, allowing developers to - * implement targeted error handling and provide more informative error messages to users. It - * maps ranges of result codes to specific error categories. - * - * <p>When constructing a {@link #newFailure} response, use the appropriate result code value to - * ensure correct categorization of the failed response. - * - * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the result code does not belong to - * any error category, for example, in the case of a successful result with {@link #RESULT_OK}. - * - * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding - * result code ranges. - */ - @ErrorCategory - public int getErrorCategory() { - if (mResultCode >= 1000 && mResultCode < 2000) { - return ERROR_CATEGORY_REQUEST_ERROR; - } - if (mResultCode >= 2000 && mResultCode < 3000) { - return ERROR_CATEGORY_SYSTEM; - } - if (mResultCode >= 3000 && mResultCode < 4000) { - return ERROR_CATEGORY_APP; - } - return ERROR_CATEGORY_UNKNOWN; - } - - /** - * Returns a generic document containing the return value of the executed function. - * - * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. - * - * <p>An empty document is returned if {@link #isSuccess} is {@code false} or if the executed - * function does not produce a return value. - * - * <p>Sample code for extracting the return value: - * - * <pre> - * GenericDocument resultDocument = response.getResultDocument(); - * Object returnValue = resultDocument.getProperty(PROPERTY_RETURN_VALUE); - * if (returnValue != null) { - * // Cast returnValue to expected type, or use {@link GenericDocument#getPropertyString}, - * // {@link GenericDocument#getPropertyLong} etc. - * // Do something with the returnValue - * } - * </pre> - */ - @NonNull - public GenericDocument getResultDocument() { - return mResultDocument; - } - - /** Returns the extras of the app function execution. */ - @NonNull - public Bundle getExtras() { - return mExtras; - } - - /** - * Returns {@code true} if {@link #getResultCode} equals {@link - * ExecuteAppFunctionResponse#RESULT_OK}. - */ - public boolean isSuccess() { - return getResultCode() == RESULT_OK; - } - - /** - * Returns one of the {@code RESULT} constants defined in {@link ExecuteAppFunctionResponse}. - */ - @ResultCode - public int getResultCode() { - return mResultCode; - } - - /** - * Returns the error message associated with this result. - * - * <p>If {@link #isSuccess} is {@code true}, the error message is always {@code null}. - */ - @Nullable - public String getErrorMessage() { - return mErrorMessage; - } - - /** - * Result codes. - * - * @hide - */ - @IntDef( - prefix = {"RESULT_"}, - value = { - RESULT_OK, - RESULT_DENIED, - RESULT_APP_UNKNOWN_ERROR, - RESULT_SYSTEM_ERROR, - RESULT_FUNCTION_NOT_FOUND, - RESULT_INVALID_ARGUMENT, - RESULT_DISABLED, - RESULT_CANCELLED - }) - @Retention(RetentionPolicy.SOURCE) - public @interface ResultCode {} - - /** - * Error categories. - * - * @hide - */ - @IntDef( - prefix = {"ERROR_CATEGORY_"}, - value = { - ERROR_CATEGORY_UNKNOWN, - ERROR_CATEGORY_REQUEST_ERROR, - ERROR_CATEGORY_APP, - ERROR_CATEGORY_SYSTEM - }) - @Retention(RetentionPolicy.SOURCE) - public @interface ErrorCategory {} -} diff --git a/libs/appfunctions/tests/Android.bp b/libs/appfunctions/tests/Android.bp index 6f5eff305d8d..db79675ae9f7 100644 --- a/libs/appfunctions/tests/Android.bp +++ b/libs/appfunctions/tests/Android.bp @@ -25,7 +25,7 @@ android_test { "androidx.test.rules", "androidx.test.ext.junit", "androidx.core_core-ktx", - "com.google.android.appfunctions.sidecar.impl", + "com.android.extensions.appfunctions.impl", "junit", "kotlin-test", "mockito-target-extended-minus-junit4", diff --git a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt b/libs/appfunctions/tests/src/com/android/extensions/appfunctions/tests/SidecarConverterTest.kt index 264f84209caf..11202d58e484 100644 --- a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt +++ b/libs/appfunctions/tests/src/com/android/extensions/appfunctions/tests/SidecarConverterTest.kt @@ -14,14 +14,15 @@ * limitations under the License. */ -package com.google.android.appfunctions.sidecar.tests +package com.android.extensions.appfunctions.tests +import android.app.appfunctions.AppFunctionException import android.app.appfunctions.ExecuteAppFunctionRequest import android.app.appfunctions.ExecuteAppFunctionResponse import android.app.appsearch.GenericDocument import android.os.Bundle import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.android.appfunctions.sidecar.SidecarConverter +import com.android.extensions.appfunctions.SidecarConverter import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -60,7 +61,7 @@ class SidecarConverterTest { .setPropertyLong("testLong", 23) .build() val sidecarRequest = - com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder( + com.android.extensions.appfunctions.ExecuteAppFunctionRequest.Builder( "targetPkg", "targetFunctionId" ) @@ -83,44 +84,38 @@ class SidecarConverterTest { GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "") .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true) .build() - val platformResponse = ExecuteAppFunctionResponse.newSuccess(resultGd, null) + val platformResponse = ExecuteAppFunctionResponse(resultGd) val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse( platformResponse ) - assertThat(sidecarResponse.isSuccess).isTrue() assertThat( sidecarResponse.resultDocument.getProperty( ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE ) ) .isEqualTo(booleanArrayOf(true)) - assertThat(sidecarResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK) - assertThat(sidecarResponse.errorMessage).isNull() } @Test - fun getSidecarExecuteAppFunctionResponse_errorResponse_sameContents() { - val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build() - val platformResponse = - ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, - null, - null + fun getSidecarAppFunctionException_sameContents() { + val bundle = Bundle() + bundle.putString("key", "value") + val platformException = + AppFunctionException( + AppFunctionException.ERROR_SYSTEM_ERROR, + "error", + bundle ) - val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse( - platformResponse + val sidecarException = SidecarConverter.getSidecarAppFunctionException( + platformException ) - assertThat(sidecarResponse.isSuccess).isFalse() - assertThat(sidecarResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace) - assertThat(sidecarResponse.resultDocument.id).isEqualTo(emptyGd.id) - assertThat(sidecarResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType) - assertThat(sidecarResponse.resultCode) - .isEqualTo(ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR) - assertThat(sidecarResponse.errorMessage).isNull() + assertThat(sidecarException.errorCode).isEqualTo(AppFunctionException.ERROR_SYSTEM_ERROR) + assertThat(sidecarException.errorMessage).isEqualTo("error") + assertThat(sidecarException.extras.getString("key")).isEqualTo("value") } @Test @@ -129,44 +124,39 @@ class SidecarConverterTest { GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "") .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true) .build() - val sidecarResponse = com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse - .newSuccess(resultGd, null) + val sidecarResponse = + com.android.extensions.appfunctions.ExecuteAppFunctionResponse(resultGd) val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse( sidecarResponse ) - assertThat(platformResponse.isSuccess).isTrue() assertThat( platformResponse.resultDocument.getProperty( ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE ) ) .isEqualTo(booleanArrayOf(true)) - assertThat(platformResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK) - assertThat(platformResponse.errorMessage).isNull() } @Test - fun getPlatformExecuteAppFunctionResponse_errorResponse_sameContents() { - val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build() - val sidecarResponse = - com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, - null, - null + fun getPlatformAppFunctionException_sameContents() { + val bundle = Bundle() + bundle.putString("key", "value") + val sidecarException = + com.android.extensions.appfunctions.AppFunctionException( + AppFunctionException.ERROR_SYSTEM_ERROR, + "error", + bundle ) - val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse( - sidecarResponse + val platformException = SidecarConverter.getPlatformAppFunctionException( + sidecarException ) - assertThat(platformResponse.isSuccess).isFalse() - assertThat(platformResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace) - assertThat(platformResponse.resultDocument.id).isEqualTo(emptyGd.id) - assertThat(platformResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType) - assertThat(platformResponse.resultCode) - .isEqualTo(ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR) - assertThat(platformResponse.errorMessage).isNull() + assertThat(platformException.errorCode) + .isEqualTo(AppFunctionException.ERROR_SYSTEM_ERROR) + assertThat(platformException.errorMessage).isEqualTo("error") + assertThat(platformException.extras.getString("key")).isEqualTo("value") } } |