diff options
46 files changed, 1275 insertions, 529 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index 42e60e419de0..57c731757cc9 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -51,6 +51,7 @@ import android.util.Slog; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.server.LocalServices; @@ -1668,7 +1669,8 @@ public final class JobStatus { return readinessStatusWithConstraint(constraint, true); } - private boolean readinessStatusWithConstraint(int constraint, boolean value) { + @VisibleForTesting + boolean readinessStatusWithConstraint(int constraint, boolean value) { boolean oldValue = false; int satisfied = mSatisfiedConstraintsOfInterest; switch (constraint) { @@ -1704,6 +1706,15 @@ public final class JobStatus { break; } + // The flexibility constraint relies on other constraints to be satisfied. + // This function lacks the information to determine if flexibility will be satisfied. + // But for the purposes of this function it is still useful to know the jobs' readiness + // not including the flexibility constraint. If flexibility is the constraint in question + // we can proceed as normal. + if (constraint != CONSTRAINT_FLEXIBLE) { + satisfied |= CONSTRAINT_FLEXIBLE; + } + boolean toReturn = isReady(satisfied); switch (constraint) { diff --git a/core/api/current.txt b/core/api/current.txt index ea2ae0c9e15f..4734e8a75de6 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -23189,9 +23189,10 @@ package android.media { public abstract static class MediaRouter2.RouteCallback { ctor public MediaRouter2.RouteCallback(); - method public void onRoutesAdded(@NonNull java.util.List<android.media.MediaRoute2Info>); - method public void onRoutesChanged(@NonNull java.util.List<android.media.MediaRoute2Info>); - method public void onRoutesRemoved(@NonNull java.util.List<android.media.MediaRoute2Info>); + method @Deprecated public void onRoutesAdded(@NonNull java.util.List<android.media.MediaRoute2Info>); + method @Deprecated public void onRoutesChanged(@NonNull java.util.List<android.media.MediaRoute2Info>); + method @Deprecated public void onRoutesRemoved(@NonNull java.util.List<android.media.MediaRoute2Info>); + method public void onRoutesUpdated(@NonNull java.util.List<android.media.MediaRoute2Info>); } public class MediaRouter2.RoutingController { diff --git a/libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml b/libs/WindowManager/Shell/res/animator/tv_window_menu_action_button_animator.xml index 7475abac4695..7475abac4695 100644 --- a/libs/WindowManager/Shell/res/animator/tv_pip_menu_action_button_animator.xml +++ b/libs/WindowManager/Shell/res/animator/tv_window_menu_action_button_animator.xml diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon.xml index ce8640df0093..67467bbc72ae 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon.xml @@ -15,5 +15,5 @@ ~ limitations under the License. --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="@color/tv_pip_menu_icon_unfocused" /> + <item android:color="@color/tv_window_menu_icon_unfocused" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon_bg.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon_bg.xml index 4f5e63dac5c0..4182bfeefa1b 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon_bg.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_close_icon_bg.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2021 The Android Open Source Project + ~ Copyright (C) 2022 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. @@ -16,6 +16,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true" - android:color="@color/tv_pip_menu_icon_bg_focused" /> - <item android:color="@color/tv_pip_menu_icon_bg_unfocused" /> + android:color="@color/tv_window_menu_close_icon_bg_focused" /> + <item android:color="@color/tv_window_menu_close_icon_bg_unfocused" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_icon.xml index 275870450493..45205d2a7138 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_icon.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_icon.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2021 The Android Open Source Project + ~ Copyright (C) 2022 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. @@ -16,8 +16,8 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true" - android:color="@color/tv_pip_menu_icon_focused" /> + android:color="@color/tv_window_menu_icon_focused" /> <item android:state_enabled="false" - android:color="@color/tv_pip_menu_icon_disabled" /> - <item android:color="@color/tv_pip_menu_icon_unfocused" /> + android:color="@color/tv_window_menu_icon_disabled" /> + <item android:color="@color/tv_window_menu_icon_unfocused" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon_bg.xml b/libs/WindowManager/Shell/res/color/tv_window_menu_icon_bg.xml index 6cbf66f00df7..1bd26e1d6583 100644 --- a/libs/WindowManager/Shell/res/color/tv_pip_menu_close_icon_bg.xml +++ b/libs/WindowManager/Shell/res/color/tv_window_menu_icon_bg.xml @@ -16,6 +16,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true" - android:color="@color/tv_pip_menu_close_icon_bg_focused" /> - <item android:color="@color/tv_pip_menu_close_icon_bg_unfocused" /> + android:color="@color/tv_window_menu_icon_bg_focused" /> + <item android:color="@color/tv_window_menu_icon_bg_unfocused" /> </selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml deleted file mode 100644 index 1938f4562e97..000000000000 --- a/libs/WindowManager/Shell/res/drawable/tv_pip_button_bg.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2020 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <corners android:radius="@dimen/pip_menu_button_radius" /> - <solid android:color="@color/tv_pip_menu_icon_bg" /> -</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml index 846fdb3e8a58..7085a2c72c86 100644 --- a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml +++ b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> <selector xmlns:android="http://schemas.android.com/apk/res/android" - android:exitFadeDuration="@integer/pip_menu_fade_animation_duration"> + android:exitFadeDuration="@integer/tv_window_menu_fade_animation_duration"> <item android:state_activated="true"> <shape android:shape="rectangle"> <corners android:radius="@dimen/pip_menu_border_corner_radius" /> diff --git a/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml b/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml new file mode 100644 index 000000000000..2dba37daf059 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/tv_window_button_bg.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 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. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/tv_window_menu_button_radius" /> + <solid android:color="@color/tv_window_menu_icon_bg" /> +</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index 2d50d3f1392d..8533a5994d33 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -64,14 +64,14 @@ android:layout_width="@dimen/pip_menu_button_wrapper_margin" android:layout_height="@dimen/pip_menu_button_wrapper_margin"/> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton + <com.android.wm.shell.common.TvWindowMenuActionButton android:id="@+id/tv_pip_menu_fullscreen_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/pip_ic_fullscreen_white" android:text="@string/pip_fullscreen" /> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton + <com.android.wm.shell.common.TvWindowMenuActionButton android:id="@+id/tv_pip_menu_close_button" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -80,14 +80,14 @@ <!-- More TvPipMenuActionButtons may be added here at runtime. --> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton + <com.android.wm.shell.common.TvWindowMenuActionButton android:id="@+id/tv_pip_menu_move_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/pip_ic_move_white" android:text="@string/pip_move" /> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton + <com.android.wm.shell.common.TvWindowMenuActionButton android:id="@+id/tv_pip_menu_expand_button" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -145,7 +145,7 @@ android:layout_margin="@dimen/pip_menu_outer_space_frame" android:background="@drawable/tv_pip_menu_border"/> - <com.android.wm.shell.pip.tv.TvPipMenuActionButton + <com.android.wm.shell.common.TvWindowMenuActionButton android:id="@+id/tv_pip_menu_done_button" android:layout_width="wrap_content" android:layout_height="wrap_content" diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml deleted file mode 100644 index db96d8de4094..000000000000 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu_action_button.xml +++ /dev/null @@ -1,40 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2020 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<!-- Layout for TvPipMenuActionButton --> -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/button" - android:layout_width="@dimen/pip_menu_button_size" - android:layout_height="@dimen/pip_menu_button_size" - android:padding="@dimen/pip_menu_button_margin" - android:stateListAnimator="@animator/tv_pip_menu_action_button_animator" - android:focusable="true"> - - <View android:id="@+id/background" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - android:duplicateParentState="true" - android:background="@drawable/tv_pip_button_bg"/> - - <ImageView android:id="@+id/icon" - android:layout_width="@dimen/pip_menu_icon_size" - android:layout_height="@dimen/pip_menu_icon_size" - android:layout_gravity="center" - android:duplicateParentState="true" - android:tint="@color/tv_pip_menu_icon" /> -</FrameLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml new file mode 100644 index 000000000000..c4dbd39c729a --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 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. + --> +<!-- Layout for TvWindowMenuActionButton --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/button" + android:layout_width="@dimen/tv_window_menu_button_size" + android:layout_height="@dimen/tv_window_menu_button_size" + android:padding="@dimen/tv_window_menu_button_margin" + android:stateListAnimator="@animator/tv_window_menu_action_button_animator" + android:focusable="true"> + + <View android:id="@+id/background" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:duplicateParentState="true" + android:background="@drawable/tv_window_button_bg"/> + + <ImageView android:id="@+id/icon" + android:layout_width="@dimen/tv_window_menu_icon_size" + android:layout_height="@dimen/tv_window_menu_icon_size" + android:layout_gravity="center" + android:duplicateParentState="true" + android:tint="@color/tv_window_menu_icon" /> +</FrameLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index 02e726fbc3bf..b45b9ec0c457 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -15,20 +15,20 @@ limitations under the License. --> <resources> - <!-- The dimensions to user for picture-in-picture action buttons. --> - <dimen name="pip_menu_button_size">48dp</dimen> - <dimen name="pip_menu_button_radius">20dp</dimen> - <dimen name="pip_menu_icon_size">20dp</dimen> - <dimen name="pip_menu_button_margin">4dp</dimen> - <dimen name="pip_menu_button_wrapper_margin">26dp</dimen> - <dimen name="pip_menu_border_width">4dp</dimen> - <integer name="pip_menu_fade_animation_duration">500</integer> + <!-- The dimensions to use for tv window menu action buttons. --> + <dimen name="tv_window_menu_button_size">48dp</dimen> + <dimen name="tv_window_menu_button_radius">20dp</dimen> + <dimen name="tv_window_menu_icon_size">20dp</dimen> + <dimen name="tv_window_menu_button_margin">4dp</dimen> + <integer name="tv_window_menu_fade_animation_duration">500</integer> <!-- The pip menu front border corner radius is 2dp smaller than the background corner radius to hide the background from showing through. --> <dimen name="pip_menu_border_corner_radius">4dp</dimen> <dimen name="pip_menu_background_corner_radius">6dp</dimen> + <dimen name="pip_menu_border_width">4dp</dimen> <dimen name="pip_menu_outer_space">24dp</dimen> + <dimen name="pip_menu_button_wrapper_margin">26dp</dimen> <!-- outer space minus border width --> <dimen name="pip_menu_outer_space_frame">20dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/colors_tv.xml b/libs/WindowManager/Shell/res/values/colors_tv.xml index fa90fe36b545..3e71c1010278 100644 --- a/libs/WindowManager/Shell/res/values/colors_tv.xml +++ b/libs/WindowManager/Shell/res/values/colors_tv.xml @@ -15,13 +15,15 @@ ~ limitations under the License. --> <resources> - <color name="tv_pip_menu_icon_focused">#0E0E0F</color> - <color name="tv_pip_menu_icon_unfocused">#F8F9FA</color> - <color name="tv_pip_menu_icon_disabled">#80868B</color> - <color name="tv_pip_menu_close_icon_bg_focused">#D93025</color> - <color name="tv_pip_menu_close_icon_bg_unfocused">#D69F261F</color> - <color name="tv_pip_menu_icon_bg_focused">#E8EAED</color> - <color name="tv_pip_menu_icon_bg_unfocused">#990E0E0F</color> + <color name="tv_window_menu_icon_focused">#0E0E0F</color> + <color name="tv_window_menu_icon_unfocused">#F8F9FA</color> + + <color name="tv_window_menu_icon_disabled">#80868B</color> + <color name="tv_window_menu_close_icon_bg_focused">#D93025</color> + <color name="tv_window_menu_close_icon_bg_unfocused">#D69F261F</color> + <color name="tv_window_menu_icon_bg_focused">#E8EAED</color> + <color name="tv_window_menu_icon_bg_unfocused">#990E0E0F</color> + <color name="tv_pip_menu_focus_border">#E8EAED</color> <color name="tv_pip_menu_background">#1E232C</color> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java index a09aab666a31..572e3335eb11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuActionButton.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2022 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.tv; +package com.android.wm.shell.common; import android.content.Context; import android.content.res.TypedArray; @@ -28,33 +28,32 @@ import android.widget.RelativeLayout; import com.android.wm.shell.R; /** - * A View that represents Pip Menu action button, such as "Fullscreen" and "Close" as well custom - * (provided by the application in Pip) and media buttons. + * A common action button for TV window menu layouts. */ -public class TvPipMenuActionButton extends RelativeLayout implements View.OnClickListener { +public class TvWindowMenuActionButton extends RelativeLayout implements View.OnClickListener { private final ImageView mIconImageView; private final View mButtonBackgroundView; private final View mButtonView; private OnClickListener mOnClickListener; - public TvPipMenuActionButton(Context context) { + public TvWindowMenuActionButton(Context context) { this(context, null, 0, 0); } - public TvPipMenuActionButton(Context context, AttributeSet attrs) { + public TvWindowMenuActionButton(Context context, AttributeSet attrs) { this(context, attrs, 0, 0); } - public TvPipMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr) { + public TvWindowMenuActionButton(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } - public TvPipMenuActionButton( + public TvWindowMenuActionButton( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); final LayoutInflater inflater = (LayoutInflater) getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.tv_pip_menu_action_button, this); + inflater.inflate(R.layout.tv_window_menu_action_button, this); mIconImageView = findViewById(R.id.icon); mButtonView = findViewById(R.id.button); @@ -129,20 +128,27 @@ public class TvPipMenuActionButton extends RelativeLayout implements View.OnClic return mButtonView.isEnabled(); } - void setIsCustomCloseAction(boolean isCustomCloseAction) { + /** + * Marks this button as a custom close action button. + * This changes the style of the action button to highlight that this action finishes the + * Picture-in-Picture activity. + * + * @param isCustomCloseAction sets or unsets this button as a custom close action button. + */ + public void setIsCustomCloseAction(boolean isCustomCloseAction) { mIconImageView.setImageTintList( getResources().getColorStateList( - isCustomCloseAction ? R.color.tv_pip_menu_close_icon - : R.color.tv_pip_menu_icon)); + isCustomCloseAction ? R.color.tv_window_menu_close_icon + : R.color.tv_window_menu_icon)); mButtonBackgroundView.setBackgroundTintList(getResources() - .getColorStateList(isCustomCloseAction ? R.color.tv_pip_menu_close_icon_bg - : R.color.tv_pip_menu_icon_bg)); + .getColorStateList(isCustomCloseAction ? R.color.tv_window_menu_close_icon_bg + : R.color.tv_window_menu_icon_bg)); } @Override public String toString() { if (mButtonView.getContentDescription() == null) { - return TvPipMenuActionButton.class.getSimpleName(); + return TvWindowMenuActionButton.class.getSimpleName(); } return mButtonView.getContentDescription().toString(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index 57d3a44ed2af..4d7c8465bcc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -56,6 +56,7 @@ import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; +import com.android.wm.shell.common.TvWindowMenuActionButton; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; @@ -79,7 +80,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private final LinearLayout mActionButtonsContainer; private final View mMenuFrameView; - private final List<TvPipMenuActionButton> mAdditionalButtons = new ArrayList<>(); + private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>(); private final View mPipFrameView; private final View mPipView; private final TextView mEduTextView; @@ -94,7 +95,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private final ImageView mArrowRight; private final ImageView mArrowDown; private final ImageView mArrowLeft; - private final TvPipMenuActionButton mA11yDoneButton; + private final TvWindowMenuActionButton mA11yDoneButton; private final ScrollView mScrollView; private final HorizontalScrollView mHorizontalScrollView; @@ -104,8 +105,8 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private boolean mMoveMenuIsVisible; private boolean mButtonMenuIsVisible; - private final TvPipMenuActionButton mExpandButton; - private final TvPipMenuActionButton mCloseButton; + private final TvWindowMenuActionButton mExpandButton; + private final TvWindowMenuActionButton mCloseButton; private boolean mSwitchingOrientation; @@ -166,7 +167,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mResizeAnimationDuration = context.getResources().getInteger( R.integer.config_pipResizeAnimationDuration); mPipMenuFadeAnimationDuration = context.getResources() - .getInteger(R.integer.pip_menu_fade_animation_duration); + .getInteger(R.integer.tv_window_menu_fade_animation_duration); mPipMenuOuterSpace = context.getResources() .getDimensionPixelSize(R.dimen.pip_menu_outer_space); @@ -568,7 +569,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { if (actionsNumber > buttonsNumber) { // Add buttons until we have enough to display all the actions. while (actionsNumber > buttonsNumber) { - TvPipMenuActionButton button = new TvPipMenuActionButton(mContext); + TvWindowMenuActionButton button = new TvWindowMenuActionButton(mContext); button.setOnClickListener(this); mActionButtonsContainer.addView(button, @@ -591,7 +592,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { // "Assign" actions to the buttons. for (int index = 0; index < actionsNumber; index++) { final RemoteAction action = actions.get(index); - final TvPipMenuActionButton button = mAdditionalButtons.get(index); + final TvWindowMenuActionButton button = mAdditionalButtons.get(index); // Remove action if it matches the custom close action. if (PipUtils.remoteActionsMatch(action, closeAction)) { @@ -607,7 +608,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { } } - private void setActionForButton(RemoteAction action, TvPipMenuActionButton button, + private void setActionForButton(RemoteAction action, TvWindowMenuActionButton button, Handler mainHandler) { button.setVisibility(View.VISIBLE); // Ensure the button is visible. if (action.getContentDescription().length() > 0) { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt new file mode 100644 index 000000000000..da954d97aec2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2022 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.flicker.splitscreen + +import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.Presubmit +import android.view.WindowManagerPolicyConstants +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.wm.shell.flicker.appWindowBecomesVisible +import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import com.android.wm.shell.flicker.layerBecomesVisible +import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible +import org.junit.Assume +import org.junit.Before +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test quick switch to split pair from another app. + * + * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromAnotherApp` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group1 +class SwitchBackToSplitFromAnotherApp(testSpec: FlickerTestParameter) : SplitScreenBase(testSpec) { + val thirdApp = SplitScreenHelper.getNonResizeable(instrumentation) + + // TODO(b/231399940): Remove this once we can use recent shortcut to enter split. + @Before + open fun before() { + Assume.assumeTrue(tapl.isTablet) + } + + override val transition: FlickerBuilder.() -> Unit + get() = { + super.transition(this) + setup { + eachRun { + primaryApp.launchViaIntent(wmHelper) + // TODO(b/231399940): Use recent shortcut to enter split. + tapl.launchedAppState.taskbar + .openAllApps() + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + + thirdApp.launchViaIntent(wmHelper) + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .withWindowSurfaceAppeared(thirdApp) + .waitForAndVerify() + } + } + transitions { + tapl.launchedAppState.quickSwitchToPreviousApp() + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + } + } + + @Presubmit + @Test + fun splitScreenDividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible() + + @Presubmit + @Test + fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(secondaryApp) + + @Presubmit + @Test + fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + primaryApp, splitLeftTop = false) + + @Presubmit + @Test + fun secondaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd( + secondaryApp, splitLeftTop = true) + + @Presubmit + @Test + fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp) + + @Presubmit + @Test + fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerIsVisibleAtStartAndEnd() = + super.navBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarLayerPositionAtStartAndEnd() = + super.navBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun navBarWindowIsAlwaysVisible() = + super.navBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerIsVisibleAtStartAndEnd() = + super.statusBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarLayerPositionAtStartAndEnd() = + super.statusBarLayerPositionAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun statusBarWindowIsAlwaysVisible() = + super.statusBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarLayerIsVisibleAtStartAndEnd() = + super.taskBarLayerIsVisibleAtStartAndEnd() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun taskBarWindowIsAlwaysVisible() = + super.taskBarWindowIsAlwaysVisible() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + /** {@inheritDoc} */ + @Postsubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + repetitions = SplitScreenHelper.TEST_REPETITIONS, + // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy. + supportedNavigationModes = + listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt index f69eb8f06182..db89ff52178b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt @@ -26,10 +26,8 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.wm.shell.flicker.appWindowBecomesVisible -import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.layerBecomesVisible -import com.android.wm.shell.flicker.layerIsVisibleAtEnd import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible import org.junit.Assume @@ -41,7 +39,7 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test switch back to split pair after go home + * Test quick switch to split pair from home. * * To run this test: `atest WMShellFlickerTests:SwitchBackToSplitFromHome` */ @@ -59,30 +57,30 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas } override val transition: FlickerBuilder.() -> Unit - get() = { - super.transition(this) - setup { - eachRun { - primaryApp.launchViaIntent(wmHelper) - // TODO(b/231399940): Use recent shortcut to enter split. - tapl.launchedAppState.taskbar - .openAllApps() - .getAppIcon(secondaryApp.appName) - .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + get() = { + super.transition(this) + setup { + eachRun { + primaryApp.launchViaIntent(wmHelper) + // TODO(b/231399940): Use recent shortcut to enter split. + tapl.launchedAppState.taskbar + .openAllApps() + .getAppIcon(secondaryApp.appName) + .dragToSplitscreen(secondaryApp.`package`, primaryApp.`package`) + SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) + + tapl.goHome() + wmHelper.StateSyncBuilder() + .withAppTransitionIdle() + .withHomeActivityVisible() + .waitForAndVerify() + } + } + transitions { + tapl.workspace.quickSwitchToPreviousApp() SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - - tapl.goHome() - wmHelper.StateSyncBuilder() - .withAppTransitionIdle() - .withHomeActivityVisible() - .waitForAndVerify() } } - transitions { - tapl.workspace.quickSwitchToPreviousApp() - SplitScreenHelper.waitForSplitComplete(wmHelper, primaryApp, secondaryApp) - } - } @Presubmit @Test @@ -90,7 +88,7 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas @Presubmit @Test - fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp) + fun primaryAppLayerBecomesVisible() = testSpec.layerBecomesVisible(primaryApp) @Presubmit @Test @@ -108,7 +106,7 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas @Presubmit @Test - fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp) + fun primaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(primaryApp) @Presubmit @Test diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl index fe15f0e67b1d..29bfd1acae17 100644 --- a/media/java/android/media/IMediaRouter2.aidl +++ b/media/java/android/media/IMediaRouter2.aidl @@ -26,9 +26,7 @@ import android.os.Bundle; oneway interface IMediaRouter2 { void notifyRouterRegistered(in List<MediaRoute2Info> currentRoutes, in RoutingSessionInfo currentSystemSessionInfo); - void notifyRoutesAdded(in List<MediaRoute2Info> routes); - void notifyRoutesRemoved(in List<MediaRoute2Info> routes); - void notifyRoutesChanged(in List<MediaRoute2Info> routes); + void notifyRoutesUpdated(in List<MediaRoute2Info> routes); void notifySessionCreated(int requestId, in @nullable RoutingSessionInfo sessionInfo); void notifySessionInfoChanged(in RoutingSessionInfo sessionInfo); void notifySessionReleased(in RoutingSessionInfo sessionInfo); diff --git a/media/java/android/media/IMediaRouter2Manager.aidl b/media/java/android/media/IMediaRouter2Manager.aidl index 71dc2a781ba9..9f3c3ff89032 100644 --- a/media/java/android/media/IMediaRouter2Manager.aidl +++ b/media/java/android/media/IMediaRouter2Manager.aidl @@ -30,8 +30,6 @@ oneway interface IMediaRouter2Manager { void notifySessionReleased(in RoutingSessionInfo session); void notifyDiscoveryPreferenceChanged(String packageName, in RouteDiscoveryPreference discoveryPreference); - void notifyRoutesAdded(in List<MediaRoute2Info> routes); - void notifyRoutesRemoved(in List<MediaRoute2Info> routes); - void notifyRoutesChanged(in List<MediaRoute2Info> routes); + void notifyRoutesUpdated(in List<MediaRoute2Info> routes); void notifyRequestFailed(int requestId, int reason); } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index a7a21e7a2013..26cb9f8e9ee1 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -132,7 +132,7 @@ public final class MediaRouter2 { /** * Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback * dispatch. This is only used to determine what callback a route should be assigned to (added, - * removed, changed) in {@link #dispatchFilteredRoutesChangedLocked(List)}. + * removed, changed) in {@link #dispatchFilteredRoutesUpdatedOnHandler(List)}. */ private volatile ArrayMap<String, MediaRoute2Info> mPreviousRoutes = new ArrayMap<>(); @@ -820,7 +820,7 @@ public final class MediaRouter2 { } } - void dispatchFilteredRoutesChangedLocked(List<MediaRoute2Info> newRoutes) { + void dispatchFilteredRoutesUpdatedOnHandler(List<MediaRoute2Info> newRoutes) { List<MediaRoute2Info> addedRoutes = new ArrayList<>(); List<MediaRoute2Info> removedRoutes = new ArrayList<>(); List<MediaRoute2Info> changedRoutes = new ArrayList<>(); @@ -863,29 +863,16 @@ public final class MediaRouter2 { if (!changedRoutes.isEmpty()) { notifyRoutesChanged(changedRoutes); } - } - void addRoutesOnHandler(List<MediaRoute2Info> routes) { - synchronized (mLock) { - for (MediaRoute2Info route : routes) { - mRoutes.put(route.getId(), route); - } - updateFilteredRoutesLocked(); + // Note: We don't notify clients of changes in route ordering. + if (!addedRoutes.isEmpty() || !removedRoutes.isEmpty() || !changedRoutes.isEmpty()) { + notifyRoutesUpdated(newRoutes); } } - void removeRoutesOnHandler(List<MediaRoute2Info> routes) { - synchronized (mLock) { - for (MediaRoute2Info route : routes) { - mRoutes.remove(route.getId()); - } - updateFilteredRoutesLocked(); - } - } - - void changeRoutesOnHandler(List<MediaRoute2Info> routes) { - List<MediaRoute2Info> changedRoutes = new ArrayList<>(); + void updateRoutesOnHandler(List<MediaRoute2Info> routes) { synchronized (mLock) { + mRoutes.clear(); for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); } @@ -900,8 +887,10 @@ public final class MediaRouter2 { Collections.unmodifiableList( filterRoutesWithCompositePreferenceLocked(List.copyOf(mRoutes.values()))); mHandler.sendMessage( - obtainMessage(MediaRouter2::dispatchFilteredRoutesChangedLocked, - this, mFilteredRoutes)); + obtainMessage( + MediaRouter2::dispatchFilteredRoutesUpdatedOnHandler, + this, + mFilteredRoutes)); } /** @@ -1211,6 +1200,14 @@ public final class MediaRouter2 { } } + private void notifyRoutesUpdated(List<MediaRoute2Info> routes) { + for (RouteCallbackRecord record : mRouteCallbackRecords) { + List<MediaRoute2Info> filteredRoutes = + filterRoutesWithIndividualPreference(routes, record.mPreference); + record.mExecutor.execute(() -> record.mRouteCallback.onRoutesUpdated(filteredRoutes)); + } + } + private void notifyPreferredFeaturesChanged(List<String> features) { for (RouteCallbackRecord record : mRouteCallbackRecords) { record.mExecutor.execute( @@ -1246,29 +1243,44 @@ public final class MediaRouter2 { /** Callback for receiving events about media route discovery. */ public abstract static class RouteCallback { /** - * Called when routes are added. Whenever you registers a callback, this will be invoked - * with known routes. + * Called when routes are added. Whenever you register a callback, this will be invoked with + * known routes. * * @param routes the list of routes that have been added. It's never empty. + * @deprecated Use {@link #onRoutesUpdated(List)} instead. */ + @Deprecated public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {} /** * Called when routes are removed. * * @param routes the list of routes that have been removed. It's never empty. + * @deprecated Use {@link #onRoutesUpdated(List)} instead. */ + @Deprecated public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {} /** - * Called when routes are changed. For example, it is called when the route's name or volume - * have been changed. + * Called when the properties of one or more existing routes are changed. For example, it is + * called when a route's name or volume have changed. * * @param routes the list of routes that have been changed. It's never empty. + * @deprecated Use {@link #onRoutesUpdated(List)} instead. */ + @Deprecated public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {} /** + * Called when the route list is updated, which can happen when routes are added, removed, + * or modified. It will also be called when a route callback is registered. + * + * @param routes the updated list of routes filtered by the callback's individual discovery + * preferences. + */ + public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {} + + /** * Called when the client app's preferred features are changed. When this is called, it is * recommended to {@link #getRoutes()} to get the routes that are currently available to the * app. @@ -1985,21 +1997,9 @@ public final class MediaRouter2 { } @Override - public void notifyRoutesAdded(List<MediaRoute2Info> routes) { - mHandler.sendMessage( - obtainMessage(MediaRouter2::addRoutesOnHandler, MediaRouter2.this, routes)); - } - - @Override - public void notifyRoutesRemoved(List<MediaRoute2Info> routes) { - mHandler.sendMessage( - obtainMessage(MediaRouter2::removeRoutesOnHandler, MediaRouter2.this, routes)); - } - - @Override - public void notifyRoutesChanged(List<MediaRoute2Info> routes) { + public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { mHandler.sendMessage( - obtainMessage(MediaRouter2::changeRoutesOnHandler, MediaRouter2.this, routes)); + obtainMessage(MediaRouter2::updateRoutesOnHandler, MediaRouter2.this, routes)); } @Override @@ -2047,17 +2047,7 @@ public final class MediaRouter2 { class ManagerCallback implements MediaRouter2Manager.Callback { @Override - public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) { - updateAllRoutesFromManager(); - } - - @Override - public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) { - updateAllRoutesFromManager(); - } - - @Override - public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) { + public void onRoutesUpdated() { updateAllRoutesFromManager(); } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 44c0b54546be..8afc7d999d2e 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -546,37 +546,15 @@ public final class MediaRouter2Manager { } } - void addRoutesOnHandler(List<MediaRoute2Info> routes) { + void updateRoutesOnHandler(@NonNull List<MediaRoute2Info> routes) { synchronized (mRoutesLock) { + mRoutes.clear(); for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); } } - if (routes.size() > 0) { - notifyRoutesAdded(routes); - } - } - void removeRoutesOnHandler(List<MediaRoute2Info> routes) { - synchronized (mRoutesLock) { - for (MediaRoute2Info route : routes) { - mRoutes.remove(route.getId()); - } - } - if (routes.size() > 0) { - notifyRoutesRemoved(routes); - } - } - - void changeRoutesOnHandler(List<MediaRoute2Info> routes) { - synchronized (mRoutesLock) { - for (MediaRoute2Info route : routes) { - mRoutes.put(route.getId(), route); - } - } - if (routes.size() > 0) { - notifyRoutesChanged(routes); - } + notifyRoutesUpdated(); } void createSessionOnHandler(int requestId, RoutingSessionInfo sessionInfo) { @@ -650,24 +628,9 @@ public final class MediaRouter2Manager { notifySessionUpdated(sessionInfo); } - private void notifyRoutesAdded(List<MediaRoute2Info> routes) { - for (CallbackRecord record: mCallbackRecords) { - record.mExecutor.execute( - () -> record.mCallback.onRoutesAdded(routes)); - } - } - - private void notifyRoutesRemoved(List<MediaRoute2Info> routes) { + private void notifyRoutesUpdated() { for (CallbackRecord record: mCallbackRecords) { - record.mExecutor.execute( - () -> record.mCallback.onRoutesRemoved(routes)); - } - } - - private void notifyRoutesChanged(List<MediaRoute2Info> routes) { - for (CallbackRecord record: mCallbackRecords) { - record.mExecutor.execute( - () -> record.mCallback.onRoutesChanged(routes)); + record.mExecutor.execute(() -> record.mCallback.onRoutesUpdated()); } } @@ -963,23 +926,12 @@ public final class MediaRouter2Manager { * Interface for receiving events about media routing changes. */ public interface Callback { - /** - * Called when routes are added. - * @param routes the list of routes that have been added. It's never empty. - */ - default void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {} /** - * Called when routes are removed. - * @param routes the list of routes that have been removed. It's never empty. + * Called when the routes list changes. This includes adding, modifying, or removing + * individual routes. */ - default void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {} - - /** - * Called when routes are changed. - * @param routes the list of routes that have been changed. It's never empty. - */ - default void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {} + default void onRoutesUpdated() {} /** * Called when a session is changed. @@ -1115,21 +1067,12 @@ public final class MediaRouter2Manager { } @Override - public void notifyRoutesAdded(List<MediaRoute2Info> routes) { - mHandler.sendMessage(obtainMessage(MediaRouter2Manager::addRoutesOnHandler, - MediaRouter2Manager.this, routes)); - } - - @Override - public void notifyRoutesRemoved(List<MediaRoute2Info> routes) { - mHandler.sendMessage(obtainMessage(MediaRouter2Manager::removeRoutesOnHandler, - MediaRouter2Manager.this, routes)); - } - - @Override - public void notifyRoutesChanged(List<MediaRoute2Info> routes) { - mHandler.sendMessage(obtainMessage(MediaRouter2Manager::changeRoutesOnHandler, - MediaRouter2Manager.this, routes)); + public void notifyRoutesUpdated(List<MediaRoute2Info> routes) { + mHandler.sendMessage( + obtainMessage( + MediaRouter2Manager::updateRoutesOnHandler, + MediaRouter2Manager.this, + routes)); } } } diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java index 4086dec99218..37c836762da0 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java @@ -32,7 +32,6 @@ import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_I import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_FIXED_VOLUME; import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_SPECIAL_FEATURE; import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_ID_VARIABLE_VOLUME; -import static com.android.mediaroutertest.StubMediaRoute2ProviderService.ROUTE_NAME2; import static com.android.mediaroutertest.StubMediaRoute2ProviderService.VOLUME_MAX; import static org.junit.Assert.assertEquals; @@ -56,10 +55,10 @@ import android.media.RoutingSessionInfo; import android.os.Bundle; import android.text.TextUtils; -import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.compatibility.common.util.PollingCheck; @@ -69,6 +68,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -115,7 +115,7 @@ public class MediaRouter2ManagerTest { @Before public void setUp() throws Exception { - mContext = InstrumentationRegistry.getTargetContext(); + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); mUiAutomation.adoptShellPermissionIdentity(Manifest.permission.MEDIA_CONTENT_CONTROL, Manifest.permission.MODIFY_AUDIO_ROUTING); @@ -170,51 +170,95 @@ public class MediaRouter2ManagerTest { } @Test - public void testOnRoutesRemovedAndAdded() throws Exception { - RouteCallback routeCallback = new RouteCallback() {}; - mRouteCallbacks.add(routeCallback); - mRouter2.registerRouteCallback(mExecutor, routeCallback, - new RouteDiscoveryPreference.Builder(FEATURES_ALL, true).build()); + public void testOnRoutesUpdated() throws Exception { + final String routeId0 = "routeId0"; + final String routeName0 = "routeName0"; + final String routeId1 = "routeId1"; + final String routeName1 = "routeName1"; + final List<String> features = Collections.singletonList("customFeature"); - Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL); + final int newConnectionState = MediaRoute2Info.CONNECTION_STATE_CONNECTED; + + final List<MediaRoute2Info> routes = new ArrayList<>(); + routes.add(new MediaRoute2Info.Builder(routeId0, routeName0).addFeatures(features).build()); + routes.add(new MediaRoute2Info.Builder(routeId1, routeName1).addFeatures(features).build()); - CountDownLatch removedLatch = new CountDownLatch(1); CountDownLatch addedLatch = new CountDownLatch(1); + CountDownLatch changedLatch = new CountDownLatch(1); + CountDownLatch removedLatch = new CountDownLatch(1); - addManagerCallback(new MediaRouter2Manager.Callback() { - @Override - public void onRoutesRemoved(List<MediaRoute2Info> routes) { - assertTrue(routes.size() > 0); - for (MediaRoute2Info route : routes) { - if (route.getOriginalId().equals(ROUTE_ID2) - && route.getName().equals(ROUTE_NAME2)) { - removedLatch.countDown(); + addManagerCallback( + new MediaRouter2Manager.Callback() { + @Override + public void onRoutesUpdated() { + if (addedLatch.getCount() == 1 + && checkRoutesMatch(mManager.getAllRoutes(), routes)) { + addedLatch.countDown(); + } else if (changedLatch.getCount() == 1 + && checkRoutesMatch( + mManager.getAllRoutes(), routes.subList(1, 2))) { + changedLatch.countDown(); + } else if (removedLatch.getCount() == 1 + && checkRoutesRemoved(mManager.getAllRoutes(), routes)) { + removedLatch.countDown(); + } } - } - } - @Override - public void onRoutesAdded(List<MediaRoute2Info> routes) { - assertTrue(routes.size() > 0); - if (removedLatch.getCount() > 0) { - return; - } - for (MediaRoute2Info route : routes) { - if (route.getOriginalId().equals(ROUTE_ID2) - && route.getName().equals(ROUTE_NAME2)) { - addedLatch.countDown(); - } - } - } - }); + }); - MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2); - assertNotNull(routeToRemove); + mService.addRoutes(routes); + assertTrue( + "Added routes not found or onRoutesUpdated() never called.", + addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); - mService.removeRoute(ROUTE_ID2); - assertTrue(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + MediaRoute2Info newRoute2 = + new MediaRoute2Info.Builder(routes.get(1)) + .setConnectionState(newConnectionState) + .build(); + routes.set(1, newRoute2); + mService.addRoute(routes.get(1)); + assertTrue( + "Modified route not found or onRoutesUpdated() never called.", + changedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + List<String> routeIds = new ArrayList<>(); + routeIds.add(routeId0); + routeIds.add(routeId1); + + mService.removeRoutes(routeIds); + assertTrue( + "Removed routes not found or onRoutesUpdated() never called.", + removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } - mService.addRoute(routeToRemove); - assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + private static boolean checkRoutesMatch( + List<MediaRoute2Info> routesReceived, List<MediaRoute2Info> expectedRoutes) { + for (MediaRoute2Info expectedRoute : expectedRoutes) { + MediaRoute2Info matchingRoute = + routesReceived.stream() + .filter(r -> r.getOriginalId().equals(expectedRoute.getOriginalId())) + .findFirst() + .orElse(null); + + if (matchingRoute == null) { + return false; + } + assertTrue(TextUtils.equals(expectedRoute.getName(), matchingRoute.getName())); + assertEquals(expectedRoute.getFeatures(), matchingRoute.getFeatures()); + assertEquals(expectedRoute.getConnectionState(), matchingRoute.getConnectionState()); + } + + return true; + } + + private static boolean checkRoutesRemoved( + List<MediaRoute2Info> routesReceived, List<MediaRoute2Info> routesRemoved) { + for (MediaRoute2Info removedRoute : routesRemoved) { + if (routesReceived.stream() + .anyMatch(r -> r.getOriginalId().equals(removedRoute.getOriginalId()))) { + return false; + } + } + return true; } @Test @@ -874,28 +918,31 @@ public class MediaRouter2ManagerTest { // A dummy callback is required to send route feature info. RouteCallback routeCallback = new RouteCallback() {}; - MediaRouter2Manager.Callback managerCallback = new MediaRouter2Manager.Callback() { - @Override - public void onRoutesAdded(List<MediaRoute2Info> routes) { - for (MediaRoute2Info route : routes) { - if (!route.isSystemRoute() - && hasMatchingFeature(route.getFeatures(), preference - .getPreferredFeatures())) { - addedLatch.countDown(); - break; + MediaRouter2Manager.Callback managerCallback = + new MediaRouter2Manager.Callback() { + @Override + public void onRoutesUpdated() { + List<MediaRoute2Info> routes = mManager.getAllRoutes(); + for (MediaRoute2Info route : routes) { + if (!route.isSystemRoute() + && hasMatchingFeature( + route.getFeatures(), + preference.getPreferredFeatures())) { + addedLatch.countDown(); + break; + } + } } - } - } - @Override - public void onDiscoveryPreferenceChanged(String packageName, - RouteDiscoveryPreference discoveryPreference) { - if (TextUtils.equals(mPackageName, packageName) - && Objects.equals(preference, discoveryPreference)) { - preferenceLatch.countDown(); - } - } - }; + @Override + public void onDiscoveryPreferenceChanged( + String packageName, RouteDiscoveryPreference discoveryPreference) { + if (TextUtils.equals(mPackageName, packageName) + && Objects.equals(preference, discoveryPreference)) { + preferenceLatch.countDown(); + } + } + }; mManager.registerCallback(mExecutor, managerCallback); mRouter2.registerRouteCallback(mExecutor, routeCallback, preference); @@ -923,15 +970,17 @@ public class MediaRouter2ManagerTest { void awaitOnRouteChangedManager(Runnable task, String routeId, Predicate<MediaRoute2Info> predicate) throws Exception { CountDownLatch latch = new CountDownLatch(1); - MediaRouter2Manager.Callback callback = new MediaRouter2Manager.Callback() { - @Override - public void onRoutesChanged(List<MediaRoute2Info> changed) { - MediaRoute2Info route = createRouteMap(changed).get(routeId); - if (route != null && predicate.test(route)) { - latch.countDown(); - } - } - }; + MediaRouter2Manager.Callback callback = + new MediaRouter2Manager.Callback() { + @Override + public void onRoutesUpdated() { + MediaRoute2Info route = + createRouteMap(mManager.getAllRoutes()).get(routeId); + if (route != null && predicate.test(route)) { + latch.countDown(); + } + } + }; mManager.registerCallback(mExecutor, callback); try { task.run(); diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java index a51e3714b6f7..a7ae5f45b795 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java @@ -30,7 +30,9 @@ import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; @@ -146,19 +148,44 @@ public class StubMediaRoute2ProviderService extends MediaRoute2ProviderService { * they have the same route id. */ public void addRoute(@NonNull MediaRoute2Info route) { - Objects.requireNonNull(route, "route must not be null"); - mRoutes.put(route.getOriginalId(), route); - publishRoutes(); + addRoutes(Collections.singletonList(route)); } /** - * Removes a route and publishes it. + * Adds a list of routes and publishes it. It will replace existing routes with matching ids. + * + * @param routes list of routes to be added. */ + public void addRoutes(@NonNull List<MediaRoute2Info> routes) { + Objects.requireNonNull(routes, "Routes must not be null."); + for (MediaRoute2Info route : routes) { + Objects.requireNonNull(route, "Route must not be null"); + mRoutes.put(route.getOriginalId(), route); + } + publishRoutes(); + } + + /** Removes a route and publishes it. */ public void removeRoute(@NonNull String routeId) { - Objects.requireNonNull(routeId, "routeId must not be null"); - MediaRoute2Info route = mRoutes.get(routeId); - if (route != null) { - mRoutes.remove(routeId); + removeRoutes(Collections.singletonList(routeId)); + } + + /** + * Removes a list of routes and publishes the changes. + * + * @param routes list of route ids to be removed. + */ + public void removeRoutes(@NonNull List<String> routes) { + Objects.requireNonNull(routes, "Routes must not be null"); + boolean hasRemovedRoutes = false; + for (String routeId : routes) { + MediaRoute2Info route = mRoutes.get(routeId); + if (route != null) { + mRoutes.remove(routeId); + hasRemovedRoutes = true; + } + } + if (hasRemovedRoutes) { publishRoutes(); } } diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle index 8c97eca548b5..d38013679ad4 100644 --- a/packages/SettingsLib/Spa/build.gradle +++ b/packages/SettingsLib/Spa/build.gradle @@ -16,9 +16,9 @@ buildscript { ext { - minSdk_version = 31 - compose_version = '1.2.0-alpha04' - compose_material3_version = '1.0.0-alpha06' + spa_min_sdk = 31 + jetpack_compose_version = '1.2.0-alpha04' + jetpack_compose_material3_version = '1.0.0-alpha06' } } plugins { diff --git a/packages/SettingsLib/Spa/codelab/AndroidManifest.xml b/packages/SettingsLib/Spa/codelab/AndroidManifest.xml index 9a89e5efdddb..36b93134bdcb 100644 --- a/packages/SettingsLib/Spa/codelab/AndroidManifest.xml +++ b/packages/SettingsLib/Spa/codelab/AndroidManifest.xml @@ -13,14 +13,14 @@ 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. - --> +--> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.spa.codelab"> <application android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/Theme.SettingsLib.Compose.DayNight"> + android:theme="@style/Theme.SpaLib.DayNight"> <activity android:name="com.android.settingslib.spa.codelab.MainActivity" android:exported="true"> diff --git a/packages/SettingsLib/Spa/codelab/build.gradle b/packages/SettingsLib/Spa/codelab/build.gradle index 5251ddd8c01d..169ecf08aeac 100644 --- a/packages/SettingsLib/Spa/codelab/build.gradle +++ b/packages/SettingsLib/Spa/codelab/build.gradle @@ -25,7 +25,7 @@ android { defaultConfig { applicationId "com.android.settingslib.spa.codelab" - minSdk minSdk_version + minSdk spa_min_sdk targetSdk 33 versionCode 1 versionName "1.0" @@ -52,7 +52,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion compose_version + kotlinCompilerExtensionVersion jetpack_compose_version } packagingOptions { resources { diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 49b5e2e84667..ad69da314735 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -24,7 +24,7 @@ android { compileSdk 33 defaultConfig { - minSdk minSdk_version + minSdk spa_min_sdk targetSdk 33 } @@ -49,7 +49,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion compose_version + kotlinCompilerExtensionVersion jetpack_compose_version } packagingOptions { resources { @@ -59,11 +59,11 @@ android { } dependencies { - api "androidx.compose.material3:material3:$compose_material3_version" - api "androidx.compose.material:material-icons-extended:$compose_version" - api "androidx.compose.runtime:runtime-livedata:$compose_version" - api "androidx.compose.ui:ui-tooling-preview:$compose_version" + api "androidx.compose.material3:material3:$jetpack_compose_material3_version" + api "androidx.compose.material:material-icons-extended:$jetpack_compose_version" + api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version" + api "androidx.compose.ui:ui-tooling-preview:$jetpack_compose_version" api 'androidx.navigation:navigation-compose:2.5.0' api 'com.google.android.material:material:1.6.1' - debugApi "androidx.compose.ui:ui-tooling:$compose_version" + debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version" } diff --git a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml b/packages/SettingsLib/Spa/spa/res/values-night/themes.xml index 8b52b507bdd9..67dd2b0cc5e0 100644 --- a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml +++ b/packages/SettingsLib/Spa/spa/res/values-night/themes.xml @@ -13,8 +13,8 @@ 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. - --> +--> <resources> - <style name="Theme.SettingsLib.Compose.DayNight" /> + <style name="Theme.SpaLib.DayNight" /> </resources> diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml index 01f9ea592f6d..e0e5fc211ec6 100644 --- a/packages/SettingsLib/Spa/spa/res/values/themes.xml +++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml @@ -13,15 +13,15 @@ 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. - --> +--> <resources> - <style name="Theme.SettingsLib.Compose" parent="Theme.Material3.DayNight.NoActionBar"> + <style name="Theme.SpaLib" parent="Theme.Material3.DayNight.NoActionBar"> <item name="android:statusBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item> </style> - <style name="Theme.SettingsLib.Compose.DayNight"> + <style name="Theme.SpaLib.DayNight"> <item name="android:windowLightStatusBar">true</item> </style> </resources> diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DrawablePainter.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DrawablePainter.kt new file mode 100644 index 000000000000..ae325f8862eb --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DrawablePainter.kt @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2022 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.settingslib.spa.framework.compose + +import android.graphics.drawable.Animatable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.os.Handler +import android.os.Looper +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.RememberObserver +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asAndroidColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.withSave +import androidx.compose.ui.unit.LayoutDirection +import kotlin.math.roundToInt + +/** + * ************************************************************************************************* + * This file was forked from + * https://github.com/google/accompanist/blob/main/drawablepainter/src/main/java/com/google/accompanist/drawablepainter/DrawablePainter.kt + * and will be removed once it lands in AndroidX. + */ + +private val MAIN_HANDLER by lazy(LazyThreadSafetyMode.NONE) { + Handler(Looper.getMainLooper()) +} + +/** + * A [Painter] which draws an Android [Drawable] and supports [Animatable] drawables. Instances + * should be remembered to be able to start and stop [Animatable] animations. + * + * Instances are usually retrieved from [rememberDrawablePainter]. + */ +class DrawablePainter( + val drawable: Drawable +) : Painter(), RememberObserver { + private var drawInvalidateTick by mutableStateOf(0) + private var drawableIntrinsicSize by mutableStateOf(drawable.intrinsicSize) + + private val callback: Drawable.Callback by lazy { + object : Drawable.Callback { + override fun invalidateDrawable(d: Drawable) { + // Update the tick so that we get re-drawn + drawInvalidateTick++ + // Update our intrinsic size too + drawableIntrinsicSize = drawable.intrinsicSize + } + + override fun scheduleDrawable(d: Drawable, what: Runnable, time: Long) { + MAIN_HANDLER.postAtTime(what, time) + } + + override fun unscheduleDrawable(d: Drawable, what: Runnable) { + MAIN_HANDLER.removeCallbacks(what) + } + } + } + + init { + if (drawable.intrinsicWidth >= 0 && drawable.intrinsicHeight >= 0) { + // Update the drawable's bounds to match the intrinsic size + drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + } + } + + override fun onRemembered() { + drawable.callback = callback + drawable.setVisible(true, true) + if (drawable is Animatable) drawable.start() + } + + override fun onAbandoned() = onForgotten() + + override fun onForgotten() { + if (drawable is Animatable) drawable.stop() + drawable.setVisible(false, false) + drawable.callback = null + } + + override fun applyAlpha(alpha: Float): Boolean { + drawable.alpha = (alpha * 255).roundToInt().coerceIn(0, 255) + return true + } + + override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { + drawable.colorFilter = colorFilter?.asAndroidColorFilter() + return true + } + + override fun applyLayoutDirection(layoutDirection: LayoutDirection): Boolean = + drawable.setLayoutDirection( + when (layoutDirection) { + LayoutDirection.Ltr -> View.LAYOUT_DIRECTION_LTR + LayoutDirection.Rtl -> View.LAYOUT_DIRECTION_RTL + } + ) + + override val intrinsicSize: Size get() = drawableIntrinsicSize + + override fun DrawScope.onDraw() { + drawIntoCanvas { canvas -> + // Reading this ensures that we invalidate when invalidateDrawable() is called + drawInvalidateTick + + // Update the Drawable's bounds + drawable.setBounds(0, 0, size.width.roundToInt(), size.height.roundToInt()) + + canvas.withSave { + drawable.draw(canvas.nativeCanvas) + } + } + } +} + +/** + * Remembers [Drawable] wrapped up as a [Painter]. This function attempts to un-wrap the + * drawable contents and use Compose primitives where possible. + * + * If the provided [drawable] is `null`, an empty no-op painter is returned. + * + * This function tries to dispatch lifecycle events to [drawable] as much as possible from + * within Compose. + * + * @sample com.google.accompanist.sample.drawablepainter.BasicSample + */ +@Composable +fun rememberDrawablePainter(drawable: Drawable?): Painter = remember(drawable) { + when (drawable) { + null -> EmptyPainter + is BitmapDrawable -> BitmapPainter(drawable.bitmap.asImageBitmap()) + is ColorDrawable -> ColorPainter(Color(drawable.color)) + // Since the DrawablePainter will be remembered and it implements RememberObserver, it + // will receive the necessary events + else -> DrawablePainter(drawable.mutate()) + } +} + +private val Drawable.intrinsicSize: Size + get() = when { + // Only return a finite size if the drawable has an intrinsic size + intrinsicWidth >= 0 && intrinsicHeight >= 0 -> { + Size(width = intrinsicWidth.toFloat(), height = intrinsicHeight.toFloat()) + } + else -> Size.Unspecified + } + +internal object EmptyPainter : Painter() { + override val intrinsicSize: Size get() = Size.Unspecified + override fun DrawScope.onDraw() {} +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt index 7c8608da6724..ba8854653b0b 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt @@ -16,9 +16,17 @@ package com.android.settingslib.spa.framework.compose +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext + +@Composable +fun <T> rememberContext(constructor: (Context) -> T): T { + val context = LocalContext.current + return remember(context) { constructor(context) } +} /** * Remember the [State] initialized with the [this]. diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt index 6bdc294d888a..9a34dbf36735 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt @@ -25,8 +25,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Divider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.ui.Alignment @@ -39,6 +37,7 @@ import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsOpacity import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.ui.SettingsTitle @Composable internal fun BaseLayout( @@ -94,11 +93,7 @@ private fun BaseIcon( @Composable private fun Titles(title: String, subTitle: @Composable () -> Unit, modifier: Modifier) { Column(modifier) { - Text( - text = title, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleMedium, - ) + SettingsTitle(title) subTitle() } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt index 3b99d36e630b..4b2c8e41a388 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt @@ -19,8 +19,6 @@ package com.android.settingslib.spa.widget.preference import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.BatteryChargingFull import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.ui.Modifier @@ -29,6 +27,7 @@ import androidx.compose.ui.unit.Dp import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.ui.SettingsBody @Composable internal fun BasePreference( @@ -44,15 +43,7 @@ internal fun BasePreference( ) { BaseLayout( title = title, - subTitle = { - if (summary.value.isNotEmpty()) { - Text( - text = summary.value, - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - ) - } - }, + subTitle = { SettingsBody(summary) }, modifier = modifier, icon = icon, enabled = enabled, diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle index 707017e7e17f..be5a5ec40c4f 100644 --- a/packages/SettingsLib/Spa/tests/build.gradle +++ b/packages/SettingsLib/Spa/tests/build.gradle @@ -24,7 +24,7 @@ android { compileSdk 33 defaultConfig { - minSdk minSdk_version + minSdk spa_min_sdk targetSdk 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -50,7 +50,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion compose_version + kotlinCompilerExtensionVersion jetpack_compose_version } packagingOptions { resources { @@ -62,6 +62,6 @@ android { dependencies { androidTestImplementation(project(":spa")) androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' - androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version") - androidTestDebugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" + androidTestImplementation("androidx.compose.ui:ui-test-junit4:$jetpack_compose_version") + androidTestDebugImplementation "androidx.compose.ui:ui-test-manifest:$jetpack_compose_version" } diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp new file mode 100644 index 000000000000..48f7ff270ac7 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/Android.bp @@ -0,0 +1,33 @@ +// +// Copyright (C) 2022 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 { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_library { + name: "SpaPrivilegedLib", + + srcs: ["src/**/*.kt"], + + static_libs: [ + "SpaLib", + "SettingsLib", + "androidx.compose.runtime_runtime", + ], + kotlincflags: ["-Xjvm-default=all"], + min_sdk_version: "31", +} diff --git a/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml new file mode 100644 index 000000000000..2efa10744bb3 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 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. +--> + +<manifest package="com.android.settingslib.spaprivileged" /> diff --git a/packages/SettingsLib/SpaPrivileged/OWNERS b/packages/SettingsLib/SpaPrivileged/OWNERS new file mode 100644 index 000000000000..9256ca5cc2b0 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/OWNERS @@ -0,0 +1 @@ +include platform/frameworks/base:/packages/SettingsLib/Spa/OWNERS diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.kt new file mode 100644 index 000000000000..a6378ef53437 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 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.settingslib.spaprivileged.framework.app + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.graphics.drawable.Drawable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.produceState +import com.android.settingslib.Utils +import com.android.settingslib.spa.framework.compose.rememberContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun rememberAppRepository(): AppRepository = rememberContext(::AppRepositoryImpl) + +interface AppRepository { + @Composable + fun produceLabel(app: ApplicationInfo): State<String> + + @Composable + fun produceIcon(app: ApplicationInfo): State<Drawable?> +} + +private class AppRepositoryImpl(private val context: Context) : AppRepository { + private val packageManager = context.packageManager + + @Composable + override fun produceLabel(app: ApplicationInfo) = produceState(initialValue = "", app) { + withContext(Dispatchers.Default) { + value = app.loadLabel(packageManager).toString() + } + } + + @Composable + override fun produceIcon(app: ApplicationInfo) = + produceState<Drawable?>(initialValue = null, app) { + withContext(Dispatchers.Default) { + value = Utils.getBadgedIcon(context, app) + } + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt new file mode 100644 index 000000000000..5a3e66619c39 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 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.settingslib.spaprivileged.framework.app + +import android.content.pm.PackageInfo +import android.content.pm.PackageManager + +object PackageManagers { + fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo = + PackageManager.getPackageInfoAsUserCached(packageName, 0, userId) +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt new file mode 100644 index 000000000000..5ae514cfb524 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 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.settingslib.spaprivileged.template.app + +import android.content.pm.ApplicationInfo +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.settingslib.spa.framework.compose.rememberDrawablePainter +import com.android.settingslib.spa.widget.ui.SettingsBody +import com.android.settingslib.spa.widget.ui.SettingsTitle +import com.android.settingslib.spaprivileged.framework.app.PackageManagers +import com.android.settingslib.spaprivileged.framework.app.rememberAppRepository + +@Composable +fun AppInfo(packageName: String, userId: Int) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally) { + val packageInfo = remember { PackageManagers.getPackageInfoAsUser(packageName, userId) } + Box(modifier = Modifier.padding(8.dp)) { + AppIcon(app = packageInfo.applicationInfo, size = 48) + } + AppLabel(packageInfo.applicationInfo) + Spacer(modifier = Modifier.height(4.dp)) + SettingsBody(packageInfo.versionName) + } +} + +@Composable +fun AppIcon(app: ApplicationInfo, size: Int) { + val appRepository = rememberAppRepository() + Image( + painter = rememberDrawablePainter(appRepository.produceIcon(app).value), + contentDescription = null, + modifier = Modifier.size(size.dp) + ) +} + +@Composable +fun AppLabel(app: ApplicationInfo) { + val appRepository = rememberAppRepository() + SettingsTitle(appRepository.produceLabel(app)) +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index d9262cce3cb9..766c036d521c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -528,7 +528,7 @@ public class InfoMediaManager extends MediaManager { class RouterManagerCallback implements MediaRouter2Manager.Callback { @Override - public void onRoutesAdded(List<MediaRoute2Info> routes) { + public void onRoutesUpdated() { refreshDevices(); } @@ -540,16 +540,6 @@ public class InfoMediaManager extends MediaManager { } @Override - public void onRoutesChanged(List<MediaRoute2Info> routes) { - refreshDevices(); - } - - @Override - public void onRoutesRemoved(List<MediaRoute2Info> routes) { - refreshDevices(); - } - - @Override public void onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) { if (DEBUG) { Log.d(TAG, "onTransferred() oldSession : " + oldSession.getName() diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index ee7b7d6b180f..f4af6e852580 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -112,7 +112,7 @@ public class InfoMediaManagerTest { final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID); assertThat(mediaDevice).isNull(); - mInfoMediaManager.mMediaRouterCallback.onRoutesAdded(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -135,7 +135,7 @@ public class InfoMediaManagerTest { assertThat(mediaDevice).isNull(); mInfoMediaManager.mPackageName = ""; - mInfoMediaManager.mMediaRouterCallback.onRoutesAdded(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -199,7 +199,7 @@ public class InfoMediaManagerTest { final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID); assertThat(mediaDevice).isNull(); - mInfoMediaManager.mMediaRouterCallback.onRoutesChanged(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -222,7 +222,7 @@ public class InfoMediaManagerTest { assertThat(mediaDevice).isNull(); mInfoMediaManager.mPackageName = ""; - mInfoMediaManager.mMediaRouterCallback.onRoutesChanged(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -263,7 +263,7 @@ public class InfoMediaManagerTest { final MediaDevice mediaDevice = mInfoMediaManager.findMediaDevice(TEST_ID); assertThat(mediaDevice).isNull(); - mInfoMediaManager.mMediaRouterCallback.onRoutesRemoved(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); @@ -286,7 +286,7 @@ public class InfoMediaManagerTest { assertThat(mediaDevice).isNull(); mInfoMediaManager.mPackageName = ""; - mInfoMediaManager.mMediaRouterCallback.onRoutesRemoved(routes); + mInfoMediaManager.mMediaRouterCallback.onRoutesUpdated(); final MediaDevice infoDevice = mInfoMediaManager.mMediaDevices.get(0); assertThat(infoDevice.getId()).isEqualTo(TEST_ID); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index e27cbeaab139..bfa8af957208 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -927,8 +927,9 @@ class MediaRouter2ServiceImpl { routerRecord.mUserRecord.mHandler, routerRecord, manager)); } - userRecord.mHandler.sendMessage(obtainMessage(UserHandler::notifyRoutesToManager, - userRecord.mHandler, manager)); + userRecord.mHandler.sendMessage( + obtainMessage( + UserHandler::notifyInitialRoutesToManager, userRecord.mHandler, manager)); } private void unregisterManagerLocked(@NonNull IMediaRouter2Manager manager, boolean died) { @@ -1311,6 +1312,36 @@ class MediaRouter2ServiceImpl { new CopyOnWriteArrayList<>(); private final Map<String, RouterRecord> mSessionToRouterMap = new ArrayMap<>(); + /** + * Latest list of routes sent to privileged {@link android.media.MediaRouter2 routers} and + * {@link android.media.MediaRouter2Manager managers}. + * + * <p>Privileged routers are instances of {@link android.media.MediaRouter2 MediaRouter2} + * that have {@code MODIFY_AUDIO_ROUTING} permission. + * + * <p>This list contains all routes exposed by route providers. This includes routes from + * both system route providers and user route providers. + * + * <p>See {@link #getRouters(boolean hasModifyAudioRoutingPermission)}. + */ + private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToPrivilegedRouters = + new ArrayMap<>(); + + /** + * Latest list of routes sent to non-privileged {@link android.media.MediaRouter2 routers}. + * + * <p>Non-privileged routers are instances of {@link android.media.MediaRouter2 + * MediaRouter2} that do <i><b>not</b></i> have {@code MODIFY_AUDIO_ROUTING} permission. + * + * <p>This list contains all routes exposed by user route providers. It might also include + * the current default route from {@link #mSystemProvider} to expose local route updates + * (e.g. volume changes) to non-privileged routers. + * + * <p>See {@link SystemMediaRoute2Provider#mDefaultRoute}. + */ + private final Map<String, MediaRoute2Info> mLastNotifiedRoutesToNonPrivilegedRouters = + new ArrayMap<>(); + private boolean mRunning; // TODO: (In Android S+) Pull out SystemMediaRoute2Provider out of UserHandler. @@ -1425,91 +1456,182 @@ class MediaRouter2ServiceImpl { } private void onProviderStateChangedOnHandler(@NonNull MediaRoute2Provider provider) { - int providerInfoIndex = getLastProviderInfoIndex(provider.getUniqueId()); MediaRoute2ProviderInfo currentInfo = provider.getProviderInfo(); + + int providerInfoIndex = + indexOfRouteProviderInfoByUniqueId(provider.getUniqueId(), mLastProviderInfos); + MediaRoute2ProviderInfo prevInfo = - (providerInfoIndex < 0) ? null : mLastProviderInfos.get(providerInfoIndex); - if (Objects.equals(prevInfo, currentInfo)) return; + providerInfoIndex == -1 ? null : mLastProviderInfos.get(providerInfoIndex); + + // Ignore if no changes + if (Objects.equals(prevInfo, currentInfo)) { + return; + } + + boolean hasAddedOrModifiedRoutes = false; + boolean hasRemovedRoutes = false; + + boolean isSystemProvider = provider.mIsSystemRouteProvider; - List<MediaRoute2Info> addedRoutes = new ArrayList<>(); - List<MediaRoute2Info> removedRoutes = new ArrayList<>(); - List<MediaRoute2Info> changedRoutes = new ArrayList<>(); if (prevInfo == null) { + // Provider is being added. mLastProviderInfos.add(currentInfo); - addedRoutes.addAll(currentInfo.getRoutes()); + addToRoutesMap(currentInfo.getRoutes(), isSystemProvider); + // Check if new provider exposes routes. + hasAddedOrModifiedRoutes = !currentInfo.getRoutes().isEmpty(); } else if (currentInfo == null) { + // Provider is being removed. + hasRemovedRoutes = true; mLastProviderInfos.remove(prevInfo); - removedRoutes.addAll(prevInfo.getRoutes()); + removeFromRoutesMap(prevInfo.getRoutes(), isSystemProvider); } else { + // Provider is being updated. mLastProviderInfos.set(providerInfoIndex, currentInfo); - final Collection<MediaRoute2Info> prevRoutes = prevInfo.getRoutes(); final Collection<MediaRoute2Info> currentRoutes = currentInfo.getRoutes(); + // Checking for individual routes. for (MediaRoute2Info route : currentRoutes) { if (!route.isValid()) { - Slog.w(TAG, "onProviderStateChangedOnHandler: Ignoring invalid route : " - + route); + Slog.w( + TAG, + "onProviderStateChangedOnHandler: Ignoring invalid route : " + + route); continue; } + MediaRoute2Info prevRoute = prevInfo.getRoute(route.getOriginalId()); - if (prevRoute == null) { - addedRoutes.add(route); - } else if (!Objects.equals(prevRoute, route)) { - changedRoutes.add(route); + if (prevRoute == null || !Objects.equals(prevRoute, route)) { + hasAddedOrModifiedRoutes = true; + mLastNotifiedRoutesToPrivilegedRouters.put(route.getId(), route); + if (!isSystemProvider) { + mLastNotifiedRoutesToNonPrivilegedRouters.put(route.getId(), route); + } } } + // Checking for individual removals for (MediaRoute2Info prevRoute : prevInfo.getRoutes()) { if (currentInfo.getRoute(prevRoute.getOriginalId()) == null) { - removedRoutes.add(prevRoute); + hasRemovedRoutes = true; + mLastNotifiedRoutesToPrivilegedRouters.remove(prevRoute.getId()); + if (!isSystemProvider) { + mLastNotifiedRoutesToNonPrivilegedRouters.remove(prevRoute.getId()); + } } } } + dispatchUpdates( + hasAddedOrModifiedRoutes, + hasRemovedRoutes, + isSystemProvider, + mSystemProvider.getDefaultRoute()); + } + + /** + * Adds provided routes to {@link #mLastNotifiedRoutesToPrivilegedRouters}. Also adds them + * to {@link #mLastNotifiedRoutesToNonPrivilegedRouters} if they were provided by a + * non-system route provider. Overwrites any route with matching id that already exists. + * + * @param routes list of routes to be added. + * @param isSystemRoutes indicates whether routes come from a system route provider. + */ + private void addToRoutesMap( + @NonNull Collection<MediaRoute2Info> routes, boolean isSystemRoutes) { + for (MediaRoute2Info route : routes) { + if (!isSystemRoutes) { + mLastNotifiedRoutesToNonPrivilegedRouters.put(route.getId(), route); + } + mLastNotifiedRoutesToPrivilegedRouters.put(route.getId(), route); + } + } + + /** + * Removes provided routes from {@link #mLastNotifiedRoutesToPrivilegedRouters}. Also + * removes them from {@link #mLastNotifiedRoutesToNonPrivilegedRouters} if they were + * provided by a non-system route provider. + * + * @param routes list of routes to be removed. + * @param isSystemRoutes whether routes come from a system route provider. + */ + private void removeFromRoutesMap( + @NonNull Collection<MediaRoute2Info> routes, boolean isSystemRoutes) { + for (MediaRoute2Info route : routes) { + if (!isSystemRoutes) { + mLastNotifiedRoutesToNonPrivilegedRouters.remove(route.getId()); + } + mLastNotifiedRoutesToPrivilegedRouters.remove(route.getId()); + } + } + + /** + * Dispatches the latest route updates in {@link #mLastNotifiedRoutesToPrivilegedRouters} + * and {@link #mLastNotifiedRoutesToNonPrivilegedRouters} to registered {@link + * android.media.MediaRouter2 routers} and {@link MediaRouter2Manager managers} after a call + * to {@link #onProviderStateChangedOnHandler(MediaRoute2Provider)}. Ignores if no changes + * were made. + * + * @param hasAddedOrModifiedRoutes whether routes were added or modified. + * @param hasRemovedRoutes whether routes were removed. + * @param isSystemProvider whether the latest update was caused by a system provider. + * @param defaultRoute the current default route in {@link #mSystemProvider}. + */ + private void dispatchUpdates( + boolean hasAddedOrModifiedRoutes, + boolean hasRemovedRoutes, + boolean isSystemProvider, + MediaRoute2Info defaultRoute) { + + // Ignore if no changes. + if (!hasAddedOrModifiedRoutes && !hasRemovedRoutes) { + return; + } + List<IMediaRouter2> routersWithModifyAudioRoutingPermission = getRouters(true); List<IMediaRouter2> routersWithoutModifyAudioRoutingPermission = getRouters(false); List<IMediaRouter2Manager> managers = getManagers(); - List<MediaRoute2Info> defaultRoute = new ArrayList<>(); - defaultRoute.add(mSystemProvider.getDefaultRoute()); - - if (addedRoutes.size() > 0) { - notifyRoutesAddedToRouters(routersWithModifyAudioRoutingPermission, addedRoutes); - if (!provider.mIsSystemRouteProvider) { - notifyRoutesAddedToRouters(routersWithoutModifyAudioRoutingPermission, - addedRoutes); - } else if (prevInfo == null) { - notifyRoutesAddedToRouters(routersWithoutModifyAudioRoutingPermission, - defaultRoute); - } // 'else' is handled as changed routes - notifyRoutesAddedToManagers(managers, addedRoutes); - } - if (removedRoutes.size() > 0) { - notifyRoutesRemovedToRouters(routersWithModifyAudioRoutingPermission, - removedRoutes); - if (!provider.mIsSystemRouteProvider) { - notifyRoutesRemovedToRouters(routersWithoutModifyAudioRoutingPermission, - removedRoutes); - } - notifyRoutesRemovedToManagers(managers, removedRoutes); - } - if (changedRoutes.size() > 0) { - notifyRoutesChangedToRouters(routersWithModifyAudioRoutingPermission, - changedRoutes); - if (!provider.mIsSystemRouteProvider) { - notifyRoutesChangedToRouters(routersWithoutModifyAudioRoutingPermission, - changedRoutes); - } else if (prevInfo != null) { - notifyRoutesChangedToRouters(routersWithoutModifyAudioRoutingPermission, - defaultRoute); - } // 'else' is handled as added routes - notifyRoutesChangedToManagers(managers, changedRoutes); - } - } - - private int getLastProviderInfoIndex(@NonNull String providerId) { - for (int i = 0; i < mLastProviderInfos.size(); i++) { - MediaRoute2ProviderInfo providerInfo = mLastProviderInfos.get(i); - if (TextUtils.equals(providerInfo.getUniqueId(), providerId)) { + + // Managers receive all provider updates with all routes. + notifyRoutesUpdatedToManagers( + managers, new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values())); + + // Routers with modify audio permission (usually system routers) receive all provider + // updates with all routes. + notifyRoutesUpdatedToRouters( + routersWithModifyAudioRoutingPermission, + new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values())); + + if (!isSystemProvider) { + // Regular routers receive updates from all non-system providers with all non-system + // routes. + notifyRoutesUpdatedToRouters( + routersWithoutModifyAudioRoutingPermission, + new ArrayList<>(mLastNotifiedRoutesToNonPrivilegedRouters.values())); + } else if (hasAddedOrModifiedRoutes) { + // On system provider updates, regular routers receive the updated default route. + // This is the only system route they should receive. + mLastNotifiedRoutesToNonPrivilegedRouters.put(defaultRoute.getId(), defaultRoute); + notifyRoutesUpdatedToRouters( + routersWithoutModifyAudioRoutingPermission, + new ArrayList<>(mLastNotifiedRoutesToNonPrivilegedRouters.values())); + } + } + + /** + * Returns the index of the first element in {@code lastProviderInfos} that matches the + * specified unique id. + * + * @param uniqueId unique id of {@link MediaRoute2ProviderInfo} to be found. + * @param lastProviderInfos list of {@link MediaRoute2ProviderInfo}. + * @return index of found element, or -1 if not found. + */ + private static int indexOfRouteProviderInfoByUniqueId( + @NonNull String uniqueId, + @NonNull List<MediaRoute2ProviderInfo> lastProviderInfos) { + for (int i = 0; i < lastProviderInfos.size(); i++) { + MediaRoute2ProviderInfo providerInfo = lastProviderInfos.get(i); + if (TextUtils.equals(providerInfo.getUniqueId(), uniqueId)) { return i; } } @@ -1989,41 +2111,19 @@ class MediaRouter2ServiceImpl { } } - private void notifyRoutesAddedToRouters(@NonNull List<IMediaRouter2> routers, - @NonNull List<MediaRoute2Info> routes) { - for (IMediaRouter2 router : routers) { - try { - router.notifyRoutesAdded(routes); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify routes added. Router probably died.", ex); - } - } - } - - private void notifyRoutesRemovedToRouters(@NonNull List<IMediaRouter2> routers, - @NonNull List<MediaRoute2Info> routes) { - for (IMediaRouter2 router : routers) { - try { - router.notifyRoutesRemoved(routes); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify routes removed. Router probably died.", ex); - } - } - } - - private void notifyRoutesChangedToRouters(@NonNull List<IMediaRouter2> routers, - @NonNull List<MediaRoute2Info> routes) { + private void notifyRoutesUpdatedToRouters( + @NonNull List<IMediaRouter2> routers, @NonNull List<MediaRoute2Info> routes) { for (IMediaRouter2 router : routers) { try { - router.notifyRoutesChanged(routes); + router.notifyRoutesUpdated(routes); } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify routes changed. Router probably died.", ex); + Slog.w(TAG, "Failed to notify routes updated. Router probably died.", ex); } } } - private void notifySessionInfoChangedToRouters(@NonNull List<IMediaRouter2> routers, - @NonNull RoutingSessionInfo sessionInfo) { + private void notifySessionInfoChangedToRouters( + @NonNull List<IMediaRouter2> routers, @NonNull RoutingSessionInfo sessionInfo) { for (IMediaRouter2 router : routers) { try { router.notifySessionInfoChanged(sessionInfo); @@ -2033,48 +2133,31 @@ class MediaRouter2ServiceImpl { } } - private void notifyRoutesToManager(@NonNull IMediaRouter2Manager manager) { - List<MediaRoute2Info> routes = new ArrayList<>(); - for (MediaRoute2ProviderInfo providerInfo : mLastProviderInfos) { - routes.addAll(providerInfo.getRoutes()); - } - if (routes.size() == 0) { + /** + * Notifies {@code manager} with all known routes. This only happens once after {@code + * manager} is registered through {@link #registerManager(IMediaRouter2Manager, String) + * registerManager()}. + * + * @param manager {@link IMediaRouter2Manager} to be notified. + */ + private void notifyInitialRoutesToManager(@NonNull IMediaRouter2Manager manager) { + if (mLastNotifiedRoutesToPrivilegedRouters.isEmpty()) { return; } try { - manager.notifyRoutesAdded(routes); + manager.notifyRoutesUpdated( + new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values())); } catch (RemoteException ex) { Slog.w(TAG, "Failed to notify all routes. Manager probably died.", ex); } } - private void notifyRoutesAddedToManagers(@NonNull List<IMediaRouter2Manager> managers, - @NonNull List<MediaRoute2Info> routes) { - for (IMediaRouter2Manager manager : managers) { - try { - manager.notifyRoutesAdded(routes); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify routes added. Manager probably died.", ex); - } - } - } - - private void notifyRoutesRemovedToManagers(@NonNull List<IMediaRouter2Manager> managers, - @NonNull List<MediaRoute2Info> routes) { - for (IMediaRouter2Manager manager : managers) { - try { - manager.notifyRoutesRemoved(routes); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to notify routes removed. Manager probably died.", ex); - } - } - } - - private void notifyRoutesChangedToManagers(@NonNull List<IMediaRouter2Manager> managers, + private void notifyRoutesUpdatedToManagers( + @NonNull List<IMediaRouter2Manager> managers, @NonNull List<MediaRoute2Info> routes) { for (IMediaRouter2Manager manager : managers) { try { - manager.notifyRoutesChanged(routes); + manager.notifyRoutesUpdated(routes); } catch (RemoteException ex) { Slog.w(TAG, "Failed to notify routes changed. Manager probably died.", ex); } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java index f15e60f32fb7..df523fedc917 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java @@ -33,6 +33,7 @@ import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONNECTIVI import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONTENT_TRIGGER; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_DEADLINE; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_DEVICE_NOT_DOZING; +import static com.android.server.job.controllers.JobStatus.CONSTRAINT_FLEXIBLE; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_IDLE; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_STORAGE_NOT_LOW; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_TIMING_DELAY; @@ -790,6 +791,83 @@ public class JobStatusTest { assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); } + @Test + public void testWouldBeReadyWithConstraint_FlexibilityDoesNotAffectReadiness() { + final JobStatus job = createJobStatus( + new JobInfo.Builder(101, new ComponentName("foo", "bar")).build()); + + markImplicitConstraintsSatisfied(job, false); + job.setFlexibilityConstraintSatisfied(0, false); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CHARGING)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_IDLE)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_BATTERY_NOT_LOW)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_STORAGE_NOT_LOW)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_TIMING_DELAY)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_DEADLINE)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CONNECTIVITY)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CONTENT_TRIGGER)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_FLEXIBLE)); + + markImplicitConstraintsSatisfied(job, true); + job.setFlexibilityConstraintSatisfied(0, false); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CHARGING)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_IDLE)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_BATTERY_NOT_LOW)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_STORAGE_NOT_LOW)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_TIMING_DELAY)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_DEADLINE)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CONNECTIVITY)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CONTENT_TRIGGER)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_FLEXIBLE)); + + markImplicitConstraintsSatisfied(job, false); + job.setFlexibilityConstraintSatisfied(0, true); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CHARGING)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_IDLE)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_BATTERY_NOT_LOW)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_STORAGE_NOT_LOW)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_TIMING_DELAY)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_DEADLINE)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CONNECTIVITY)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_CONTENT_TRIGGER)); + assertFalse(job.wouldBeReadyWithConstraint(CONSTRAINT_FLEXIBLE)); + + markImplicitConstraintsSatisfied(job, true); + job.setFlexibilityConstraintSatisfied(0, true); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CHARGING)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_IDLE)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_BATTERY_NOT_LOW)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_STORAGE_NOT_LOW)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_TIMING_DELAY)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_DEADLINE)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CONNECTIVITY)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_CONTENT_TRIGGER)); + assertTrue(job.wouldBeReadyWithConstraint(CONSTRAINT_FLEXIBLE)); + } + + @Test + public void testReadinessStatusWithConstraint_FlexibilityConstraint() { + final JobStatus job = createJobStatus( + new JobInfo.Builder(101, new ComponentName("foo", "bar")).build()); + job.setConstraintSatisfied(CONSTRAINT_FLEXIBLE, sElapsedRealtimeClock.millis(), false); + markImplicitConstraintsSatisfied(job, true); + assertTrue(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, true)); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, false)); + + markImplicitConstraintsSatisfied(job, false); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, true)); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, false)); + + job.setConstraintSatisfied(CONSTRAINT_FLEXIBLE, sElapsedRealtimeClock.millis(), true); + markImplicitConstraintsSatisfied(job, true); + assertTrue(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, true)); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, false)); + + markImplicitConstraintsSatisfied(job, false); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, true)); + assertFalse(job.readinessStatusWithConstraint(CONSTRAINT_FLEXIBLE, false)); + } + private void markImplicitConstraintsSatisfied(JobStatus job, boolean isSatisfied) { job.setQuotaConstraintSatisfied(sElapsedRealtimeClock.millis(), isSatisfied); job.setTareWealthConstraintSatisfied(sElapsedRealtimeClock.millis(), isSatisfied); @@ -797,7 +875,6 @@ public class JobStatusTest { sElapsedRealtimeClock.millis(), isSatisfied, false); job.setBackgroundNotRestrictedConstraintSatisfied( sElapsedRealtimeClock.millis(), isSatisfied, false); - job.setFlexibilityConstraintSatisfied(sElapsedRealtimeClock.millis(), isSatisfied); } private static JobStatus createJobStatus(long earliestRunTimeElapsedMillis, |