diff options
27 files changed, 1250 insertions, 148 deletions
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java index 0b92b93f2c88..874e3f4ae26a 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java @@ -47,6 +47,8 @@ import java.util.List; public class AccessibilityShortcutChooserActivity extends Activity { @ShortcutType private final int mShortcutType = ACCESSIBILITY_SHORTCUT_KEY; + private static final String KEY_ACCESSIBILITY_SHORTCUT_MENU_MODE = + "accessibility_shortcut_menu_mode"; private final List<AccessibilityTarget> mTargets = new ArrayList<>(); private AlertDialog mMenuDialog; private AlertDialog mPermissionDialog; @@ -66,14 +68,30 @@ public class AccessibilityShortcutChooserActivity extends Activity { mMenuDialog = createMenuDialog(); mMenuDialog.setOnShowListener(dialog -> updateDialogListeners()); mMenuDialog.show(); + + if (savedInstanceState != null) { + final int restoreShortcutMenuMode = + savedInstanceState.getInt(KEY_ACCESSIBILITY_SHORTCUT_MENU_MODE, + ShortcutMenuMode.LAUNCH); + if (restoreShortcutMenuMode == ShortcutMenuMode.EDIT) { + onEditButtonClicked(); + } + } } @Override protected void onDestroy() { + mMenuDialog.setOnDismissListener(null); mMenuDialog.dismiss(); super.onDestroy(); } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(KEY_ACCESSIBILITY_SHORTCUT_MENU_MODE, mTargetAdapter.getShortcutMenuMode()); + } + private void onTargetSelected(AdapterView<?> parent, View view, int position, long id) { final AccessibilityTarget target = mTargets.get(position); target.onSelected(); diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index e53d379ea92a..63d61fc5ef9e 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -5876,7 +5876,6 @@ android:excludeFromRecents="true" android:documentLaunchMode="never" android:relinquishTaskIdentity="true" - android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden" android:process=":ui" android:visibleToInstantApps="true"> <intent-filter> diff --git a/core/res/res/layout/accessibility_shortcut_chooser_item.xml b/core/res/res/layout/accessibility_shortcut_chooser_item.xml index 7cca1292af68..4d7946b2138b 100644 --- a/core/res/res/layout/accessibility_shortcut_chooser_item.xml +++ b/core/res/res/layout/accessibility_shortcut_chooser_item.xml @@ -39,15 +39,20 @@ android:layout_height="48dp" android:scaleType="fitCenter"/> - <TextView - android:id="@+id/accessibility_shortcut_target_label" + <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:layout_weight="1" - android:textSize="20sp" - android:textColor="?attr/textColorPrimary" - android:fontFamily="sans-serif-medium"/> + android:layout_weight="1"> + + <TextView + android:id="@+id/accessibility_shortcut_target_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="20sp" + android:textColor="?attr/textColorPrimary" + android:fontFamily="sans-serif-medium"/> + </LinearLayout> <TextView android:id="@+id/accessibility_shortcut_target_status" diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 5db9ddfac870..ee75a8d6f4b2 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -2443,8 +2443,7 @@ public class AudioManager { /** * Return a handle to the optional platform's {@link Spatializer} - * @return {@code null} if spatialization is not supported, the {@code Spatializer} instance - * otherwise. + * @return the {@code Spatializer} instance. */ public @Nullable Spatializer getSpatializer() { int level = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java index 3ed8b58959a1..c64bf2c60117 100644 --- a/media/java/android/media/Spatializer.java +++ b/media/java/android/media/Spatializer.java @@ -114,6 +114,7 @@ public class Spatializer { /** @hide */ @IntDef(flag = false, value = { + SPATIALIZER_IMMERSIVE_LEVEL_OTHER, SPATIALIZER_IMMERSIVE_LEVEL_NONE, SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL, }) @@ -122,21 +123,46 @@ public class Spatializer { /** * @hide + * Constant indicating the {@code Spatializer} on this device supports a spatialization + * mode that differs from the ones available at this SDK level. + * @see #getImmersiveAudioLevel() + */ + public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1; + + /** + * @hide * Constant indicating there are no spatialization capabilities supported on this device. - * @see AudioManager#getImmersiveAudioLevel() + * @see #getImmersiveAudioLevel() */ public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0; /** * @hide - * Constant indicating the {@link Spatializer} on this device supports multichannel + * Constant indicating the {@code Spatializer} on this device supports multichannel * spatialization. - * @see AudioManager#getImmersiveAudioLevel() + * @see #getImmersiveAudioLevel() */ public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1; /** * @hide + * Return the level of support for the spatialization feature on this device. + * This level of support is independent of whether the {@code Spatializer} is currently + * enabled or available and will not change over time. + * @return the level of spatialization support + * @see #isEnabled() + * @see #isAvailable() + */ + public @ImmersiveAudioLevel int getImmersiveAudioLevel() { + int level = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + try { + level = mAm.getService().getSpatializerImmersiveAudioLevel(); + } catch (Exception e) { /* using NONE */ } + return level; + } + + /** + * @hide * Enables / disables the spatializer effect. * Changing the enabled state will trigger the public * {@link OnSpatializerStateChangedListener#onSpatializerEnabledChanged(Spatializer, boolean)} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt index e5933e6a9aea..9010d5154156 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimator.kt @@ -36,6 +36,13 @@ class UnfoldMoveFromCenterAnimator @JvmOverloads constructor( * [View.setTranslationY] */ private val translationApplier: TranslationApplier = object : TranslationApplier {}, + /** + * Allows to set custom implementation for getting + * view location. Could be useful if logical view bounds + * are different than actual bounds (e.g. view container may + * have larger width than width of the items in the container) + */ + private val viewCenterProvider: ViewCenterProvider = object : ViewCenterProvider {} ) : UnfoldTransitionProgressProvider.TransitionProgressListener { private val screenSize = Point() @@ -43,6 +50,8 @@ class UnfoldMoveFromCenterAnimator @JvmOverloads constructor( private val animatedViews: MutableList<AnimatedView> = arrayListOf() + private var lastAnimationProgress: Float = 0f + /** * Updates display properties in order to calculate the initial position for the views * Must be called before [registerViewForAnimation] @@ -58,6 +67,19 @@ class UnfoldMoveFromCenterAnimator @JvmOverloads constructor( } /** + * If target view positions have changed (e.g. because of layout changes) call this method + * to re-query view positions and update the translations + */ + fun updateViewPositions() { + animatedViews.forEach { animatedView -> + animatedView.view.get()?.let { + animatedView.updateAnimatedView(it) + } + } + onTransitionProgress(lastAnimationProgress) + } + + /** * Registers a view to be animated, the view should be measured and layouted * After finishing the animation it is necessary to clear * the views using [clearRegisteredViews] @@ -85,45 +107,30 @@ class UnfoldMoveFromCenterAnimator @JvmOverloads constructor( ) } } + lastAnimationProgress = progress } - private fun createAnimatedView(view: View): AnimatedView { - val viewCenter = getViewCenter(view) + private fun createAnimatedView(view: View): AnimatedView = + AnimatedView(view = WeakReference(view)).updateAnimatedView(view) + + private fun AnimatedView.updateAnimatedView(view: View): AnimatedView { + val viewCenter = Point() + viewCenterProvider.getViewCenter(view, viewCenter) + val viewCenterX = viewCenter.x val viewCenterY = viewCenter.y - val translationX: Float - val translationY: Float - if (isVerticalFold) { val distanceFromScreenCenterToViewCenter = screenSize.x / 2 - viewCenterX - translationX = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE - translationY = 0f + startTranslationX = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE + startTranslationY = 0f } else { val distanceFromScreenCenterToViewCenter = screenSize.y / 2 - viewCenterY - translationX = 0f - translationY = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE + startTranslationX = 0f + startTranslationY = distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE } - return AnimatedView( - view = WeakReference(view), - startTranslationX = translationX, - startTranslationY = translationY - ) - } - - private fun getViewCenter(view: View): Point { - val viewLocation = IntArray(2) - view.getLocationOnScreen(viewLocation) - - val viewX = viewLocation[0] - val viewY = viewLocation[1] - - val outPoint = Point() - outPoint.x = viewX + view.width / 2 - outPoint.y = viewY + view.height / 2 - - return outPoint + return this } /** @@ -139,10 +146,29 @@ class UnfoldMoveFromCenterAnimator @JvmOverloads constructor( } } + /** + * Interface that allows to use custom logic to get the center of the view + */ + interface ViewCenterProvider { + /** + * Called when we need to get the center of the view + */ + fun getViewCenter(view: View, outPoint: Point) { + val viewLocation = IntArray(2) + view.getLocationOnScreen(viewLocation) + + val viewX = viewLocation[0] + val viewY = viewLocation[1] + + outPoint.x = viewX + view.width / 2 + outPoint.y = viewY + view.height / 2 + } + } + private class AnimatedView( val view: WeakReference<View>, - val startTranslationX: Float, - val startTranslationY: Float + var startTranslationX: Float = 0f, + var startTranslationY: Float = 0f ) } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java index 3167070e2c3e..ecc3245e286a 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java @@ -30,6 +30,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_I import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; import android.app.StatusBarManager; import android.app.StatusBarManager.WindowVisibleState; @@ -46,6 +47,7 @@ import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.shared.recents.utilities.Utilities; +import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.statusbar.CommandQueue; import javax.inject.Inject; @@ -126,6 +128,8 @@ public class TaskbarDelegate implements CommandQueue.Callbacks, .setFlag(SYSUI_STATE_NAV_BAR_HIDDEN, !isWindowVisible()) .setFlag(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY, allowSystemGestureIgnoringBarVisibility()) + .setFlag(SYSUI_STATE_SCREEN_PINNING, + ActivityManagerWrapper.getInstance().isScreenPinningActive()) .commitUpdate(mDisplayId); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java index 51eb496c3c2a..abee7a51f91f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java @@ -225,19 +225,19 @@ public class LightBarController implements BatteryController.BatteryStateChangeC } } + // If no one is light, all icons become white. + if (numLightStacks == 0) { + mStatusBarIconController.getTransitionsController().setIconsDark( + false, animateChange()); + } + // If all stacks are light, all icons get dark. - if (numLightStacks == numStacks) { + else if (numLightStacks == numStacks) { mStatusBarIconController.setIconsDarkArea(null); mStatusBarIconController.getTransitionsController().setIconsDark(true, animateChange()); } - // If no one is light, all icons become white. - else if (numLightStacks == 0) { - mStatusBarIconController.getTransitionsController().setIconsDark( - false, animateChange()); - } - // Not the same for every stack, magic! else { mStatusBarIconController.setIconsDarkArea( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.java deleted file mode 100644 index b36b67dc02c0..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone; - -import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.util.ViewController; - -/** Controller for {@link PhoneStatusBarView}. */ -public class PhoneStatusBarViewController extends ViewController<PhoneStatusBarView> { - - protected PhoneStatusBarViewController( - PhoneStatusBarView view, - CommandQueue commandQueue) { - super(view); - mView.setPanelEnabledProvider(commandQueue::panelsEnabled); - } - - @Override - protected void onViewAttached() { - } - - @Override - protected void onViewDetached() { - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt new file mode 100644 index 000000000000..9799533a568d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.phone + +import android.graphics.Point +import android.view.View +import android.view.ViewGroup +import com.android.systemui.R +import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.util.ViewController + +/** Controller for [PhoneStatusBarView]. */ +class PhoneStatusBarViewController( + view: PhoneStatusBarView, + commandQueue: CommandQueue, + statusBarMoveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController? +) : ViewController<PhoneStatusBarView>(view) { + + override fun onViewAttached() {} + override fun onViewDetached() {} + + init { + mView.setPanelEnabledProvider { + commandQueue.panelsEnabled() + } + + statusBarMoveFromCenterAnimationController?.let { animationController -> + val statusBarLeftSide: View = mView.findViewById(R.id.status_bar_left_side) + val systemIconArea: ViewGroup = mView.findViewById(R.id.system_icon_area) + + val viewCenterProvider = StatusBarViewsCenterProvider() + val viewsToAnimate = arrayOf( + statusBarLeftSide, + systemIconArea + ) + + animationController.init(viewsToAnimate, viewCenterProvider) + + mView.addOnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ -> + val widthChanged = right - left != oldRight - oldLeft + if (widthChanged) { + statusBarMoveFromCenterAnimationController.onStatusBarWidthChanged() + } + } + } + } + + private class StatusBarViewsCenterProvider : UnfoldMoveFromCenterAnimator.ViewCenterProvider { + override fun getViewCenter(view: View, outPoint: Point) = + when (view.id) { + R.id.status_bar_left_side -> { + // items aligned to the start, return start center point + getViewEdgeCenter(view, outPoint, isStart = true) + } + R.id.system_icon_area -> { + // items aligned to the end, return end center point + getViewEdgeCenter(view, outPoint, isStart = false) + } + else -> super.getViewCenter(view, outPoint) + } + + /** + * Returns start or end (based on [isStart]) center point of the view + */ + private fun getViewEdgeCenter(view: View, outPoint: Point, isStart: Boolean) { + val isRtl = view.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL + val isLeftEdge = isRtl xor isStart + + val viewLocation = IntArray(2) + view.getLocationOnScreen(viewLocation) + + val viewX = viewLocation[0] + val viewY = viewLocation[1] + + outPoint.x = viewX + if (isLeftEdge) view.height / 2 else view.width - view.height / 2 + outPoint.y = viewY + view.height / 2 + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 685b0625b9a2..32c4a0d4673b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -536,6 +536,7 @@ public class StatusBar extends SystemUI implements private final FeatureFlags mFeatureFlags; private final UnfoldTransitionConfig mUnfoldTransitionConfig; private final Lazy<UnfoldLightRevealOverlayAnimation> mUnfoldLightRevealOverlayAnimation; + private final Lazy<StatusBarMoveFromCenterAnimationController> mMoveFromCenterAnimation; private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private final MessageRouter mMessageRouter; private final WallpaperManager mWallpaperManager; @@ -768,6 +769,7 @@ public class StatusBar extends SystemUI implements BrightnessSlider.Factory brightnessSliderFactory, UnfoldTransitionConfig unfoldTransitionConfig, Lazy<UnfoldLightRevealOverlayAnimation> unfoldLightRevealOverlayAnimation, + Lazy<StatusBarMoveFromCenterAnimationController> statusBarUnfoldAnimationController, OngoingCallController ongoingCallController, SystemStatusAnimationScheduler animationScheduler, StatusBarLocationPublisher locationPublisher, @@ -860,6 +862,7 @@ public class StatusBar extends SystemUI implements mBrightnessSliderFactory = brightnessSliderFactory; mUnfoldTransitionConfig = unfoldTransitionConfig; mUnfoldLightRevealOverlayAnimation = unfoldLightRevealOverlayAnimation; + mMoveFromCenterAnimation = statusBarUnfoldAnimationController; mOngoingCallController = ongoingCallController; mAnimationScheduler = animationScheduler; mStatusBarLocationPublisher = locationPublisher; @@ -1141,8 +1144,13 @@ public class StatusBar extends SystemUI implements sendInitialExpansionAmount(listener); } + StatusBarMoveFromCenterAnimationController moveFromCenterAnimation = null; + if (mUnfoldTransitionConfig.isEnabled()) { + moveFromCenterAnimation = mMoveFromCenterAnimation.get(); + } mPhoneStatusBarViewController = - new PhoneStatusBarViewController(mStatusBarView, mCommandQueue); + new PhoneStatusBarViewController(mStatusBarView, mCommandQueue, + moveFromCenterAnimation); mPhoneStatusBarViewController.init(); mBatteryMeterViewController = new BatteryMeterViewController( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt new file mode 100644 index 000000000000..8af03aa2a3be --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarMoveFromCenterAnimationController.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.phone + +import android.view.View +import android.view.WindowManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator +import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator.ViewCenterProvider +import com.android.systemui.unfold.UnfoldTransitionProgressProvider +import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import javax.inject.Inject + +@SysUISingleton +class StatusBarMoveFromCenterAnimationController @Inject constructor( + private val unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider, + private val windowManager: WindowManager +) { + + private lateinit var moveFromCenterAnimator: UnfoldMoveFromCenterAnimator + + fun init(viewsToAnimate: Array<View>, viewCenterProvider: ViewCenterProvider) { + moveFromCenterAnimator = UnfoldMoveFromCenterAnimator(windowManager, + viewCenterProvider = viewCenterProvider) + + unfoldTransitionProgressProvider.addCallback(object : TransitionProgressListener { + override fun onTransitionStarted() { + moveFromCenterAnimator.updateDisplayProperties() + + viewsToAnimate.forEach { + moveFromCenterAnimator.registerViewForAnimation(it) + } + } + + override fun onTransitionFinished() { + moveFromCenterAnimator.onTransitionFinished() + moveFromCenterAnimator.clearRegisteredViews() + } + + override fun onTransitionProgress(progress: Float) { + moveFromCenterAnimator.onTransitionProgress(progress) + } + }) + } + + fun onStatusBarWidthChanged() { + moveFromCenterAnimator.updateViewPositions() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java index 63ee701425ed..befea41a9919 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java @@ -90,6 +90,7 @@ import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.StatusBarLocationPublisher; +import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController; import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter; import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager; import com.android.systemui.statusbar.phone.StatusBarWindowView; @@ -213,6 +214,7 @@ public interface StatusBarPhoneModule { BrightnessSlider.Factory brightnessSliderFactory, UnfoldTransitionConfig unfoldTransitionConfig, Lazy<UnfoldLightRevealOverlayAnimation> unfoldLightRevealOverlayAnimation, + Lazy<StatusBarMoveFromCenterAnimationController> statusBarMoveFromCenterAnimation, OngoingCallController ongoingCallController, SystemStatusAnimationScheduler animationScheduler, StatusBarLocationPublisher locationPublisher, @@ -307,6 +309,7 @@ public interface StatusBarPhoneModule { brightnessSliderFactory, unfoldTransitionConfig, unfoldLightRevealOverlayAnimation, + statusBarMoveFromCenterAnimation, ongoingCallController, animationScheduler, locationPublisher, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt index ebc6f2aa6e9a..6a68b71f639b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldMoveFromCenterAnimatorTest.kt @@ -157,6 +157,21 @@ class UnfoldMoveFromCenterAnimatorTest : SysuiTestCase() { assertThat(view.translationY).isWithin(0.01f).of(3.75f) } + @Test + fun testUpdateViewPositions_viewOnTheLeftAndMovedToTheRight_viewTranslatedToTheLeft() { + givenScreen(width = 100, height = 100, rotation = ROTATION_0) + val view = createView(x = 20) + animator.registerViewForAnimation(view) + animator.onTransitionStarted() + animator.onTransitionProgress(0.5f) + view.updateMock(x = 80) // view moved from the left side to the right + + animator.updateViewPositions() + + // Negative translationX -> translated to the left + assertThat(view.translationX).isWithin(0.1f).of(-5.25f) + } + private fun createView( x: Int = 0, y: Int = 0, @@ -176,7 +191,30 @@ class UnfoldMoveFromCenterAnimatorTest : SysuiTestCase() { whenever(view.width).thenReturn(width) whenever(view.height).thenReturn(height) - return view.apply { + view.updateMock(x, y, width, height, translationX, translationY) + + return view + } + + private fun View.updateMock( + x: Int = 0, + y: Int = 0, + width: Int = 10, + height: Int = 10, + translationX: Float = 0f, + translationY: Float = 0f + ) { + doAnswer { + val location = (it.arguments[0] as IntArray) + location[0] = x + location[1] = y + Unit + }.`when`(this).getLocationOnScreen(any()) + + whenever(this.width).thenReturn(width) + whenever(this.height).thenReturn(height) + + this.apply { setTranslationX(translationX) setTranslationY(translationY) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index d63730d596d0..c7d4794bd962 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -16,7 +16,10 @@ package com.android.systemui.statusbar.phone +import android.view.LayoutInflater +import android.widget.FrameLayout import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.CommandQueue import com.google.common.truth.Truth.assertThat @@ -24,7 +27,10 @@ import org.junit.Before import org.junit.Test import org.mockito.Mock import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import com.android.systemui.R +import com.android.systemui.util.mockito.any @SmallTest class PhoneStatusBarViewControllerTest : SysuiTestCase() { @@ -32,14 +38,22 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Mock private lateinit var commandQueue: CommandQueue + @Mock + private lateinit var moveFromCenterAnimation: StatusBarMoveFromCenterAnimationController + private lateinit var view: PhoneStatusBarView private lateinit var controller: PhoneStatusBarViewController @Before fun setUp() { MockitoAnnotations.initMocks(this) - view = PhoneStatusBarView(mContext, null) - controller = PhoneStatusBarViewController(view, commandQueue) + // create the view on main thread as it requires main looper + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val parent = FrameLayout(mContext) // add parent to keep layout params + view = LayoutInflater.from(mContext) + .inflate(R.layout.status_bar, parent, false) as PhoneStatusBarView + } + controller = PhoneStatusBarViewController(view, commandQueue, null) } @Test @@ -56,4 +70,11 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { assertThat(providerUsed).isTrue() } + + @Test + fun constructor_moveFromCenterAnimationIsNotNull_moveFromCenterAnimationInitialized() { + controller = PhoneStatusBarViewController(view, commandQueue, moveFromCenterAnimation) + + verify(moveFromCenterAnimation).init(any(), any()) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java index b23414bacf10..3c0382b6efec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java @@ -254,6 +254,7 @@ public class StatusBarTest extends SysuiTestCase { @Mock private BrightnessSlider.Factory mBrightnessSliderFactory; @Mock private UnfoldTransitionConfig mUnfoldTransitionConfig; @Mock private Lazy<UnfoldLightRevealOverlayAnimation> mUnfoldLightRevealOverlayAnimationLazy; + @Mock private Lazy<StatusBarMoveFromCenterAnimationController> mMoveFromCenterAnimationLazy; @Mock private OngoingCallController mOngoingCallController; @Mock private SystemStatusAnimationScheduler mAnimationScheduler; @Mock private StatusBarLocationPublisher mLocationPublisher; @@ -428,6 +429,7 @@ public class StatusBarTest extends SysuiTestCase { mBrightnessSliderFactory, mUnfoldTransitionConfig, mUnfoldLightRevealOverlayAnimationLazy, + mMoveFromCenterAnimationLazy, mOngoingCallController, mAnimationScheduler, mLocationPublisher, diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index b9b90c0f64ea..f2ba42c6ceea 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -200,7 +200,8 @@ import java.util.stream.Collectors; */ public class AudioService extends IAudioService.Stub implements AccessibilityManager.TouchExplorationStateChangeListener, - AccessibilityManager.AccessibilityServicesStateChangeListener { + AccessibilityManager.AccessibilityServicesStateChangeListener, + AudioSystemAdapter.OnRoutingUpdatedListener { private static final String TAG = "AS.AudioService"; @@ -314,12 +315,14 @@ public class AudioService extends IAudioService.Stub private static final int MSG_SET_A2DP_DEV_CONNECTION_STATE = 38; private static final int MSG_A2DP_DEV_CONFIG_CHANGE = 39; private static final int MSG_DISPATCH_AUDIO_MODE = 40; + private static final int MSG_ROUTING_UPDATED = 41; // start of messages handled under wakelock // these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(), // and not with sendMsg(..., ..., SENDMSG_QUEUE, ...) private static final int MSG_DISABLE_AUDIO_FOR_UID = 100; private static final int MSG_INIT_STREAMS_VOLUMES = 101; + private static final int MSG_INIT_SPATIALIZER = 102; // end of messages handled under wakelock // retry delay in case of failure to indicate system ready to AudioFlinger @@ -869,6 +872,8 @@ public class AudioService extends IAudioService.Stub mSfxHelper = new SoundEffectsHelper(mContext); + mSpatializerHelper = new SpatializerHelper(this, mAudioSystem); + mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); mHasVibrator = mVibrator == null ? false : mVibrator.hasVibrator(); @@ -1033,6 +1038,9 @@ public class AudioService extends IAudioService.Stub // done with service initialization, continue additional work in our Handler thread queueMsgUnderWakeLock(mAudioHandler, MSG_INIT_STREAMS_VOLUMES, 0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */); + queueMsgUnderWakeLock(mAudioHandler, MSG_INIT_SPATIALIZER, + 0 /* arg1 */, 0 /* arg2 */, null /* obj */, 0 /* delay */); + } /** @@ -1222,6 +1230,22 @@ public class AudioService extends IAudioService.Stub updateVibratorInfos(); } + //----------------------------------------------------------------- + // routing monitoring from AudioSystemAdapter + @Override + public void onRoutingUpdatedFromNative() { + sendMsg(mAudioHandler, + MSG_ROUTING_UPDATED, + SENDMSG_REPLACE, 0, 0, null, + /*delay*/ 0); + } + + void monitorRoutingChanges(boolean enabled) { + mAudioSystem.setRoutingListener(enabled ? this : null); + } + + + //----------------------------------------------------------------- RoleObserver mRoleObserver; class RoleObserver implements OnRoleHoldersChangedListener { @@ -1406,6 +1430,9 @@ public class AudioService extends IAudioService.Stub } } + // TODO check property if feature enabled + mSpatializerHelper.reset(/* featureEnabled */ true); + onIndicateSystemReady(); // indicate the end of reconfiguration phase to audio HAL AudioSystem.setParameters("restarting=false"); @@ -7539,6 +7566,13 @@ public class AudioService extends IAudioService.Stub mAudioEventWakeLock.release(); break; + case MSG_INIT_SPATIALIZER: + mSpatializerHelper.init(); + // TODO read property to see if enabled + mSpatializerHelper.setFeatureEnabled(true); + mAudioEventWakeLock.release(); + break; + case MSG_CHECK_MUSIC_ACTIVE: onCheckMusicActive((String) msg.obj); break; @@ -7671,6 +7705,10 @@ public class AudioService extends IAudioService.Stub case MSG_DISPATCH_AUDIO_MODE: dispatchMode(msg.arg1); break; + + case MSG_ROUTING_UPDATED: + mSpatializerHelper.onRoutingUpdated(); + break; } } } @@ -8239,7 +8277,7 @@ public class AudioService extends IAudioService.Stub } //========================================================================================== - private final SpatializerHelper mSpatializerHelper = new SpatializerHelper(); + private final @NonNull SpatializerHelper mSpatializerHelper; private void enforceModifyDefaultAudioEffectsPermission() { if (mContext.checkCallingOrSelfPermission( @@ -8249,9 +8287,12 @@ public class AudioService extends IAudioService.Stub } } - /** @see AudioManager#getSpatializerImmersiveAudioLevel() */ + /** + * Returns the immersive audio level that the platform is capable of + * @see Spatializer#getImmersiveAudioLevel() + */ public int getSpatializerImmersiveAudioLevel() { - return mSpatializerHelper.getImmersiveAudioLevel(); + return mSpatializerHelper.getCapableImmersiveAudioLevel(); } /** @see Spatializer#isEnabled() */ @@ -8267,7 +8308,7 @@ public class AudioService extends IAudioService.Stub /** @see Spatializer#setSpatializerEnabled(boolean) */ public void setSpatializerEnabled(boolean enabled) { enforceModifyDefaultAudioEffectsPermission(); - mSpatializerHelper.setEnabled(enabled); + mSpatializerHelper.setFeatureEnabled(enabled); } /** @see Spatializer#canBeSpatialized() */ diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index 6d567807f357..ac212eee21e6 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -17,6 +17,7 @@ package com.android.server.audio; import android.annotation.NonNull; +import android.annotation.Nullable; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioSystem; @@ -24,6 +25,8 @@ import android.media.audiopolicy.AudioMix; import android.os.SystemClock; import android.util.Log; +import com.android.internal.annotations.GuardedBy; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -59,6 +62,9 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback { private ConcurrentHashMap<AudioAttributes, ArrayList<AudioDeviceAttributes>> mDevicesForAttrCache; private int[] mMethodCacheHit; + private static final Object sRoutingListenerLock = new Object(); + @GuardedBy("sRoutingListenerLock") + private static @Nullable OnRoutingUpdatedListener sRoutingListener; /** * should be false except when trying to debug caching errors. When true, the value retrieved @@ -76,6 +82,23 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback { Log.d(TAG, "---- onRoutingUpdated (from native) ----------"); } invalidateRoutingCache(); + final OnRoutingUpdatedListener listener; + synchronized (sRoutingListenerLock) { + listener = sRoutingListener; + } + if (listener != null) { + listener.onRoutingUpdatedFromNative(); + } + } + + interface OnRoutingUpdatedListener { + void onRoutingUpdatedFromNative(); + } + + static void setRoutingListener(@Nullable OnRoutingUpdatedListener listener) { + synchronized (sRoutingListenerLock) { + sRoutingListener = listener; + } } /** diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 708d9e10ad73..2ca100c91043 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -17,9 +17,13 @@ package com.android.server.audio; import android.annotation.NonNull; +import android.annotation.Nullable; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioFormat; +import android.media.AudioSystem; +import android.media.INativeSpatializerCallback; +import android.media.ISpatializer; import android.media.ISpatializerCallback; import android.media.Spatializer; import android.os.RemoteCallbackList; @@ -28,6 +32,7 @@ import android.util.Log; import java.util.ArrayList; import java.util.List; +import java.util.Locale; /** * A helper class to manage Spatializer related functionality @@ -35,12 +40,167 @@ import java.util.List; public class SpatializerHelper { private static final String TAG = "AS.SpatializerHelper"; + private static final boolean DEBUG = true; + + private static void logd(String s) { + if (DEBUG) { + Log.i(TAG, s); + } + } + + private final @NonNull AudioSystemAdapter mASA; + private final @NonNull AudioService mAudioService; + + //------------------------------------------------------------ + // Spatializer state machine + private static final int STATE_UNINITIALIZED = 0; + private static final int STATE_NOT_SUPPORTED = 1; + private static final int STATE_DISABLED_UNAVAILABLE = 3; + private static final int STATE_ENABLED_UNAVAILABLE = 4; + private static final int STATE_ENABLED_AVAILABLE = 5; + private static final int STATE_DISABLED_AVAILABLE = 6; + private int mState = STATE_UNINITIALIZED; + + /** current level as reported by native Spatializer in callback */ + private int mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + private int mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + private @Nullable ISpatializer mSpat; + private @Nullable SpatializerCallback mSpatCallback; + + // default attributes and format that determine basic availability of spatialization + private static final AudioAttributes DEFAULT_ATTRIBUTES = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .build(); + private static final AudioFormat DEFAULT_FORMAT = new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(48000) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .build(); + // device array to store the routing for the default attributes and format, size 1 because + // media is never expected to be duplicated + private static final AudioDeviceAttributes[] ROUTING_DEVICES = new AudioDeviceAttributes[1]; //--------------------------------------------------------------- // audio device compatibility / enabled private final ArrayList<AudioDeviceAttributes> mCompatibleAudioDevices = new ArrayList<>(0); + //------------------------------------------------------ + // initialization + SpatializerHelper(@NonNull AudioService mother, @NonNull AudioSystemAdapter asa) { + mAudioService = mother; + mASA = asa; + } + + synchronized void init() { + Log.i(TAG, "Initializing"); + if (mState != STATE_UNINITIALIZED) { + throw new IllegalStateException(("init() called in state:" + mState)); + } + // is there a spatializer? + mSpatCallback = new SpatializerCallback(); + final ISpatializer spat = AudioSystem.getSpatializer(mSpatCallback); + if (spat == null) { + Log.i(TAG, "init(): No Spatializer found"); + mState = STATE_NOT_SUPPORTED; + return; + } + // capabilities of spatializer? + try { + byte[] levels = spat.getSupportedLevels(); + if (levels == null + || levels.length == 0 + || (levels.length == 1 + && levels[0] == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE)) { + Log.e(TAG, "Spatializer is useless"); + mState = STATE_NOT_SUPPORTED; + return; + } + for (byte level : levels) { + logd("found support for level: " + level); + if (level == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL) { + logd("Setting Spatializer to LEVEL_MULTICHANNEL"); + mCapableSpatLevel = level; + break; + } + } + } catch (RemoteException e) { /* capable level remains at NONE*/ } + if (mCapableSpatLevel == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { + mState = STATE_NOT_SUPPORTED; + return; + } + mState = STATE_DISABLED_UNAVAILABLE; + // note at this point mSpat is still not instantiated + } + + /** + * Like init() but resets the state and spatializer levels + * @param featureEnabled + */ + synchronized void reset(boolean featureEnabled) { + Log.i(TAG, "Resetting"); + mState = STATE_UNINITIALIZED; + mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + init(); + setFeatureEnabled(featureEnabled); + } + + //------------------------------------------------------ + // routing monitoring + void onRoutingUpdated() { + switch (mState) { + case STATE_UNINITIALIZED: + case STATE_NOT_SUPPORTED: + return; + case STATE_DISABLED_UNAVAILABLE: + case STATE_ENABLED_UNAVAILABLE: + case STATE_ENABLED_AVAILABLE: + case STATE_DISABLED_AVAILABLE: + break; + } + mASA.getDevicesForAttributes(DEFAULT_ATTRIBUTES).toArray(ROUTING_DEVICES); + final boolean able = + AudioSystem.canBeSpatialized(DEFAULT_ATTRIBUTES, DEFAULT_FORMAT, ROUTING_DEVICES); + logd("onRoutingUpdated: can spatialize media 5.1:" + able + + " on device:" + ROUTING_DEVICES[0]); + setDispatchAvailableState(able); + } + + //------------------------------------------------------ + // spatializer callback from native + private final class SpatializerCallback extends INativeSpatializerCallback.Stub { + + public void onLevelChanged(byte level) { + logd("SpatializerCallback.onLevelChanged level:" + level); + synchronized (SpatializerHelper.this) { + mSpatLevel = level; + } + // TODO use reported spat level to change state + } + + public void onHeadTrackingModeChanged(byte mode) { + logd("SpatializerCallback.onHeadTrackingModeChanged mode:" + mode); + } + + public void onHeadToSoundStagePoseUpdated(float[] headToStage) { + if (headToStage == null) { + Log.e(TAG, "SpatializerCallback.onHeadToStagePoseUpdated null transform"); + return; + } + if (DEBUG) { + // 6 values * (4 digits + 1 dot + 2 brackets) = 42 characters + StringBuilder t = new StringBuilder(42); + for (float val : headToStage) { + t.append("[").append(String.format(Locale.ENGLISH, "%.3f", val)).append("]"); + } + logd("SpatializerCallback.onHeadToStagePoseUpdated headToStage:" + t); + } + } + }; + + //------------------------------------------------------ + // compatible devices /** * @return a shallow copy of the list of compatible audio devices */ @@ -59,37 +219,72 @@ public class SpatializerHelper { } //------------------------------------------------------ - // enabled state - - // global state of feature - boolean mFeatureEnabled = false; - // initialized state, checked after each audio_server start - boolean mInitialized = false; + // states synchronized boolean isEnabled() { - return mFeatureEnabled; + switch (mState) { + case STATE_UNINITIALIZED: + case STATE_NOT_SUPPORTED: + case STATE_DISABLED_UNAVAILABLE: + case STATE_DISABLED_AVAILABLE: + return false; + case STATE_ENABLED_UNAVAILABLE: + case STATE_ENABLED_AVAILABLE: + default: + return true; + } } synchronized boolean isAvailable() { - if (!mInitialized) { - return false; + switch (mState) { + case STATE_UNINITIALIZED: + case STATE_NOT_SUPPORTED: + case STATE_ENABLED_UNAVAILABLE: + case STATE_DISABLED_UNAVAILABLE: + return false; + case STATE_DISABLED_AVAILABLE: + case STATE_ENABLED_AVAILABLE: + default: + return true; } - // TODO check device compatibility - // ... - return true; } - synchronized void setEnabled(boolean enabled) { - final boolean oldState = mFeatureEnabled; - mFeatureEnabled = enabled; - if (oldState != enabled) { - dispatchEnabledState(); + synchronized void setFeatureEnabled(boolean enabled) { + switch (mState) { + case STATE_UNINITIALIZED: + if (enabled) { + throw(new IllegalStateException("Can't enable when uninitialized")); + } + return; + case STATE_NOT_SUPPORTED: + if (enabled) { + Log.e(TAG, "Can't enable when unsupported"); + } + return; + case STATE_DISABLED_UNAVAILABLE: + case STATE_DISABLED_AVAILABLE: + if (enabled) { + createSpat(); + break; + } else { + // already in disabled state + return; + } + case STATE_ENABLED_UNAVAILABLE: + case STATE_ENABLED_AVAILABLE: + if (!enabled) { + releaseSpat(); + break; + } else { + // already in enabled state + return; + } } + setDispatchFeatureEnabledState(enabled); } - public int getImmersiveAudioLevel() { - // TODO replace placeholder code with actual effect discovery - return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + synchronized int getCapableImmersiveAudioLevel() { + return mCapableSpatLevel; } final RemoteCallbackList<ISpatializerCallback> mStateCallbacks = @@ -105,24 +300,168 @@ public class SpatializerHelper { mStateCallbacks.unregister(callback); } - private synchronized void dispatchEnabledState() { + /** + * precondition: mState = STATE_* + * isFeatureEnabled() != featureEnabled + * @param featureEnabled + */ + private synchronized void setDispatchFeatureEnabledState(boolean featureEnabled) { + if (featureEnabled) { + switch (mState) { + case STATE_DISABLED_UNAVAILABLE: + mState = STATE_ENABLED_UNAVAILABLE; + break; + case STATE_DISABLED_AVAILABLE: + mState = STATE_ENABLED_AVAILABLE; + break; + default: + throw(new IllegalStateException("Invalid mState:" + mState + + " for enabled true")); + } + } else { + switch (mState) { + case STATE_ENABLED_UNAVAILABLE: + mState = STATE_DISABLED_UNAVAILABLE; + break; + case STATE_ENABLED_AVAILABLE: + mState = STATE_DISABLED_AVAILABLE; + break; + default: + throw (new IllegalStateException("Invalid mState:" + mState + + " for enabled false")); + } + } final int nbCallbacks = mStateCallbacks.beginBroadcast(); for (int i = 0; i < nbCallbacks; i++) { try { mStateCallbacks.getBroadcastItem(i) - .dispatchSpatializerEnabledChanged(mFeatureEnabled); + .dispatchSpatializerEnabledChanged(featureEnabled); } catch (RemoteException e) { Log.e(TAG, "Error in dispatchSpatializerEnabledChanged", e); } } mStateCallbacks.finishBroadcast(); + // TODO persist enabled state + } + + private synchronized void setDispatchAvailableState(boolean available) { + switch (mState) { + case STATE_UNINITIALIZED: + case STATE_NOT_SUPPORTED: + throw(new IllegalStateException( + "Should not update available state in state:" + mState)); + case STATE_DISABLED_UNAVAILABLE: + if (available) { + mState = STATE_DISABLED_AVAILABLE; + break; + } else { + // already in unavailable state + return; + } + case STATE_ENABLED_UNAVAILABLE: + if (available) { + mState = STATE_ENABLED_AVAILABLE; + break; + } else { + // already in unavailable state + return; + } + case STATE_DISABLED_AVAILABLE: + if (available) { + // already in available state + return; + } else { + mState = STATE_DISABLED_UNAVAILABLE; + break; + } + case STATE_ENABLED_AVAILABLE: + if (available) { + // already in available state + return; + } else { + mState = STATE_ENABLED_UNAVAILABLE; + break; + } + } + final int nbCallbacks = mStateCallbacks.beginBroadcast(); + for (int i = 0; i < nbCallbacks; i++) { + try { + mStateCallbacks.getBroadcastItem(i) + .dispatchSpatializerAvailableChanged(available); + } catch (RemoteException e) { + Log.e(TAG, "Error in dispatchSpatializerEnabledChanged", e); + } + } + mStateCallbacks.finishBroadcast(); + } + + //------------------------------------------------------ + // native Spatializer management + + /** + * precondition: mState == STATE_DISABLED_* + */ + private void createSpat() { + if (mSpat == null) { + mSpatCallback = new SpatializerCallback(); + mSpat = AudioSystem.getSpatializer(mSpatCallback); + try { + mSpat.setLevel((byte) Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL); + } catch (RemoteException e) { + Log.e(TAG, "Can't set spatializer level", e); + mState = STATE_NOT_SUPPORTED; + mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + } + } + } + + /** + * precondition: mState == STATE_ENABLED_* + */ + private void releaseSpat() { + if (mSpat != null) { + mSpatCallback = null; + try { + mSpat.release(); + mSpat = null; + } catch (RemoteException e) { + Log.e(TAG, "Can't set release spatializer cleanly", e); + } + } } //------------------------------------------------------ // virtualization capabilities synchronized boolean canBeSpatialized( @NonNull AudioAttributes attributes, @NonNull AudioFormat format) { - // TODO hook up to spatializer effect for query - return false; + logd("canBeSpatialized usage:" + attributes.getUsage() + + " format:" + format.toLogFriendlyString()); + switch (mState) { + case STATE_UNINITIALIZED: + case STATE_NOT_SUPPORTED: + case STATE_ENABLED_UNAVAILABLE: + case STATE_DISABLED_UNAVAILABLE: + logd("canBeSpatialized false due to state:" + mState); + return false; + case STATE_DISABLED_AVAILABLE: + case STATE_ENABLED_AVAILABLE: + break; + } + + // filter on AudioAttributes usage + switch (attributes.getUsage()) { + case AudioAttributes.USAGE_MEDIA: + case AudioAttributes.USAGE_GAME: + break; + default: + logd("canBeSpatialized false due to usage:" + attributes.getUsage()); + return false; + } + AudioDeviceAttributes[] devices = + // going through adapter to take advantage of routing cache + (AudioDeviceAttributes[]) mASA.getDevicesForAttributes(attributes).toArray(); + final boolean able = AudioSystem.canBeSpatialized(attributes, format, devices); + logd("canBeSpatialized returning " + able); + return able; } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 5174a38d5edc..0ba77d8552d3 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -30,6 +30,7 @@ import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; import android.os.Bundle; import android.os.IBinder; +import android.os.LocaleList; import android.os.RemoteException; import android.service.voice.IVoiceInteractionSession; import android.util.IntArray; @@ -611,6 +612,14 @@ public abstract class ActivityTaskManagerInternal { PackageConfigurationUpdater setNightMode(int nightMode); /** + * Sets the app-specific locales for the application referenced by this updater. + * This setting is persisted and will overlay on top of the system locales for + * the said application. + * @return the current {@link PackageConfigurationUpdater} updated with the provided locale. + */ + PackageConfigurationUpdater setLocales(LocaleList locales); + + /** * Commit changes. */ void commit(); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 859107c5da5e..1c8f6f1f851d 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -956,7 +956,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { setRecentTasks(new RecentTasks(this, mTaskSupervisor)); mVrController = new VrController(mGlobalLock); mKeyguardController = mTaskSupervisor.getKeyguardController(); - mPackageConfigPersister = new PackageConfigPersister(mTaskSupervisor.mPersisterQueue); + mPackageConfigPersister = new PackageConfigPersister(mTaskSupervisor.mPersisterQueue, this); } public void onActivityManagerInternalAdded() { @@ -6575,7 +6575,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { final class PackageConfigurationUpdaterImpl implements ActivityTaskManagerInternal.PackageConfigurationUpdater { private final int mPid; - private int mNightMode; + private Integer mNightMode; + private LocaleList mLocales; PackageConfigurationUpdaterImpl(int pid) { mPid = pid; @@ -6588,6 +6589,13 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override + public ActivityTaskManagerInternal.PackageConfigurationUpdater + setLocales(LocaleList locales) { + mLocales = locales; + return this; + } + + @Override public void commit() { synchronized (mGlobalLock) { final long ident = Binder.clearCallingIdentity(); @@ -6597,8 +6605,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { Slog.w(TAG, "Override application configuration: cannot find pid " + mPid); return; } - wpc.setOverrideNightMode(mNightMode); - wpc.updateNightModeForAllActivities(mNightMode); + LocaleList localesOverride = LocaleOverlayHelper.combineLocalesIfOverlayExists( + mLocales, getGlobalConfiguration().getLocales()); + wpc.applyAppSpecificConfig(mNightMode, localesOverride); + wpc.updateAppSpecificSettingsForAllActivities(mNightMode, localesOverride); mPackageConfigPersister.updateFromImpl(wpc.mName, wpc.mUserId, this); } finally { Binder.restoreCallingIdentity(ident); @@ -6606,8 +6616,12 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } } - int getNightMode() { + Integer getNightMode() { return mNightMode; } + + LocaleList getLocales() { + return mLocales; + } } } diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java index 6fafc0291427..eeb85c585876 100644 --- a/services/core/java/com/android/server/wm/ConfigurationContainer.java +++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java @@ -39,6 +39,7 @@ import android.app.WindowConfiguration; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; +import android.os.LocaleList; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; @@ -512,7 +513,7 @@ public abstract class ConfigurationContainer<E extends ConfigurationContainer> { return mFullConfiguration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_FREEFORM; } - /** Returns the activity type associated with the the configuration container. */ + /** Returns the activity type associated with the configuration container. */ /*@WindowConfiguration.ActivityType*/ public int getActivityType() { return mFullConfiguration.windowConfiguration.getActivityType(); @@ -546,20 +547,48 @@ public abstract class ConfigurationContainer<E extends ConfigurationContainer> { } /** + * Applies app-specific nightMode and {@link LocaleList} on requested configuration. + * @return true if any of the requested configuration has been updated. + */ + public boolean applyAppSpecificConfig(Integer nightMode, LocaleList locales) { + mRequestsTmpConfig.setTo(getRequestedOverrideConfiguration()); + boolean newNightModeSet = (nightMode != null) && setOverrideNightMode(mRequestsTmpConfig, + nightMode); + boolean newLocalesSet = (locales != null) && setOverrideLocales(mRequestsTmpConfig, + locales); + if (newNightModeSet || newLocalesSet) { + onRequestedOverrideConfigurationChanged(mRequestsTmpConfig); + } + return newNightModeSet || newLocalesSet; + } + + /** * Overrides the night mode applied to this ConfigurationContainer. * @return true if the nightMode has been changed. */ - public boolean setOverrideNightMode(int nightMode) { + private boolean setOverrideNightMode(Configuration requestsTmpConfig, int nightMode) { final int currentUiMode = mRequestedOverrideConfiguration.uiMode; final int currentNightMode = currentUiMode & Configuration.UI_MODE_NIGHT_MASK; final int validNightMode = nightMode & Configuration.UI_MODE_NIGHT_MASK; if (currentNightMode == validNightMode) { return false; } - mRequestsTmpConfig.setTo(getRequestedOverrideConfiguration()); - mRequestsTmpConfig.uiMode = validNightMode + requestsTmpConfig.uiMode = validNightMode | (currentUiMode & ~Configuration.UI_MODE_NIGHT_MASK); - onRequestedOverrideConfigurationChanged(mRequestsTmpConfig); + return true; + } + + /** + * Overrides the locales applied to this ConfigurationContainer. + * @return true if the LocaleList has been changed. + */ + private boolean setOverrideLocales(Configuration requestsTmpConfig, + @NonNull LocaleList overrideLocales) { + if (mRequestedOverrideConfiguration.getLocales().equals(overrideLocales)) { + return false; + } + requestsTmpConfig.setLocales(overrideLocales); + requestsTmpConfig.userSetLocale = true; return true; } diff --git a/services/core/java/com/android/server/wm/LocaleOverlayHelper.java b/services/core/java/com/android/server/wm/LocaleOverlayHelper.java new file mode 100644 index 000000000000..a1a01dba769a --- /dev/null +++ b/services/core/java/com/android/server/wm/LocaleOverlayHelper.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2021 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.server.wm; + +import android.os.LocaleList; + +import java.util.Locale; + +/** + * Static utilities to overlay locales on top of another localeList. + * + * <p>This is used to overlay application-specific locales in + * {@link com.android.server.wm.ActivityTaskManagerInternal.PackageConfigurationUpdater} on top of + * system locales. + */ +final class LocaleOverlayHelper { + + /** + * Combines the overlay locales and base locales. + * @return the combined {@link LocaleList} if the overlay locales is not empty/null else + * returns the empty/null LocaleList. + */ + static LocaleList combineLocalesIfOverlayExists(LocaleList overlayLocales, + LocaleList baseLocales) { + if (overlayLocales == null || overlayLocales.isEmpty()) { + return overlayLocales; + } + return combineLocales(overlayLocales, baseLocales); + } + + /** + * Creates a combined {@link LocaleList} by placing overlay locales before base locales and + * dropping duplicates from the base locales. + */ + private static LocaleList combineLocales(LocaleList overlayLocales, LocaleList baseLocales) { + Locale[] combinedLocales = new Locale[overlayLocales.size() + baseLocales.size()]; + for (int i = 0; i < overlayLocales.size(); i++) { + combinedLocales[i] = overlayLocales.get(i); + } + for (int i = 0; i < baseLocales.size(); i++) { + combinedLocales[i + overlayLocales.size()] = baseLocales.get(i); + } + // Constructor of {@link LocaleList} removes duplicates + return new LocaleList(combinedLocales); + } + + +} diff --git a/services/core/java/com/android/server/wm/PackageConfigPersister.java b/services/core/java/com/android/server/wm/PackageConfigPersister.java index 1552a96d699a..505c4beb8fdc 100644 --- a/services/core/java/com/android/server/wm/PackageConfigPersister.java +++ b/services/core/java/com/android/server/wm/PackageConfigPersister.java @@ -21,6 +21,7 @@ import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; import android.annotation.NonNull; import android.os.Environment; +import android.os.LocaleList; import android.util.AtomicFile; import android.util.Slog; import android.util.SparseArray; @@ -54,12 +55,14 @@ public class PackageConfigPersister { private static final String TAG_CONFIG = "config"; private static final String ATTR_PACKAGE_NAME = "package_name"; private static final String ATTR_NIGHT_MODE = "night_mode"; + private static final String ATTR_LOCALES = "locale_list"; private static final String PACKAGE_DIRNAME = "package_configs"; private static final String SUFFIX_FILE_NAME = "_config.xml"; private final PersisterQueue mPersisterQueue; private final Object mLock = new Object(); + private final ActivityTaskManagerService mAtm; @GuardedBy("mLock") private final SparseArray<HashMap<String, PackageConfigRecord>> mPendingWrite = @@ -72,8 +75,9 @@ public class PackageConfigPersister { return new File(Environment.getDataSystemCeDirectory(userId), PACKAGE_DIRNAME); } - PackageConfigPersister(PersisterQueue queue) { + PackageConfigPersister(PersisterQueue queue, ActivityTaskManagerService atm) { mPersisterQueue = queue; + mAtm = atm; } @GuardedBy("mLock") @@ -100,7 +104,8 @@ public class PackageConfigPersister { final TypedXmlPullParser in = Xml.resolvePullParser(is); int event; String packageName = null; - int nightMode = MODE_NIGHT_AUTO; + Integer nightMode = null; + LocaleList locales = null; while (((event = in.next()) != XmlPullParser.END_DOCUMENT) && event != XmlPullParser.END_TAG) { final String name = in.getName(); @@ -120,6 +125,9 @@ public class PackageConfigPersister { case ATTR_NIGHT_MODE: nightMode = Integer.parseInt(attrValue); break; + case ATTR_LOCALES: + locales = LocaleList.forLanguageTags(attrValue); + break; } } } @@ -130,6 +138,7 @@ public class PackageConfigPersister { final PackageConfigRecord initRecord = findRecordOrCreate(mModified, packageName, userId); initRecord.mNightMode = nightMode; + initRecord.mLocales = locales; if (DEBUG) { Slog.d(TAG, "loadPackages: load one package " + initRecord); } @@ -155,7 +164,9 @@ public class PackageConfigPersister { "updateConfigIfNeeded record " + container + " find? " + modifiedRecord); } if (modifiedRecord != null) { - container.setOverrideNightMode(modifiedRecord.mNightMode); + container.applyAppSpecificConfig(modifiedRecord.mNightMode, + LocaleOverlayHelper.combineLocalesIfOverlayExists( + modifiedRecord.mLocales, mAtm.getGlobalConfiguration().getLocales())); } } } @@ -165,10 +176,16 @@ public class PackageConfigPersister { ActivityTaskManagerService.PackageConfigurationUpdaterImpl impl) { synchronized (mLock) { PackageConfigRecord record = findRecordOrCreate(mModified, packageName, userId); - record.mNightMode = impl.getNightMode(); - - if (record.isResetNightMode()) { - removePackage(record.mName, record.mUserId); + if (impl.getNightMode() != null) { + record.mNightMode = impl.getNightMode(); + } + if (impl.getLocales() != null) { + record.mLocales = impl.getLocales(); + } + if ((record.mNightMode == null || record.isResetNightMode()) + && (record.mLocales == null || record.mLocales.isEmpty())) { + // if all values default to system settings, we can remove the package. + removePackage(packageName, userId); } else { final PackageConfigRecord pendingRecord = findRecord(mPendingWrite, record.mName, record.mUserId); @@ -179,10 +196,11 @@ public class PackageConfigPersister { } else { writeRecord = pendingRecord; } - if (writeRecord.mNightMode == record.mNightMode) { + + if (!updateNightMode(record, writeRecord) && !updateLocales(record, writeRecord)) { return; } - writeRecord.mNightMode = record.mNightMode; + if (DEBUG) { Slog.d(TAG, "PackageConfigUpdater save config " + writeRecord); } @@ -191,6 +209,22 @@ public class PackageConfigPersister { } } + private boolean updateNightMode(PackageConfigRecord record, PackageConfigRecord writeRecord) { + if (record.mNightMode == null || record.mNightMode.equals(writeRecord.mNightMode)) { + return false; + } + writeRecord.mNightMode = record.mNightMode; + return true; + } + + private boolean updateLocales(PackageConfigRecord record, PackageConfigRecord writeRecord) { + if (record.mLocales == null || record.mLocales.equals(writeRecord.mLocales)) { + return false; + } + writeRecord.mLocales = record.mLocales; + return true; + } + @GuardedBy("mLock") void removeUser(int userId) { synchronized (mLock) { @@ -210,7 +244,7 @@ public class PackageConfigPersister { @GuardedBy("mLock") void onPackageUninstall(String packageName) { synchronized (mLock) { - for (int i = mModified.size() - 1; i > 0; i--) { + for (int i = mModified.size() - 1; i >= 0; i--) { final int userId = mModified.keyAt(i); removePackage(packageName, userId); } @@ -242,7 +276,8 @@ public class PackageConfigPersister { static class PackageConfigRecord { final String mName; final int mUserId; - int mNightMode; + Integer mNightMode; + LocaleList mLocales; PackageConfigRecord(String name, int userId) { mName = name; @@ -256,7 +291,7 @@ public class PackageConfigPersister { @Override public String toString() { return "PackageConfigRecord package name: " + mName + " userId " + mUserId - + " nightMode " + mNightMode; + + " nightMode " + mNightMode + " locales " + mLocales; } } @@ -369,7 +404,13 @@ public class PackageConfigPersister { } xmlSerializer.startTag(null, TAG_CONFIG); xmlSerializer.attribute(null, ATTR_PACKAGE_NAME, mRecord.mName); - xmlSerializer.attributeInt(null, ATTR_NIGHT_MODE, mRecord.mNightMode); + if (mRecord.mNightMode != null) { + xmlSerializer.attributeInt(null, ATTR_NIGHT_MODE, mRecord.mNightMode); + } + if (mRecord.mLocales != null) { + xmlSerializer.attribute(null, ATTR_LOCALES, mRecord.mLocales + .toLanguageTags()); + } xmlSerializer.endTag(null, TAG_CONFIG); xmlSerializer.endDocument(); xmlSerializer.flush(); diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index cd29f0eb61a2..6eb2e8a2fd54 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -57,6 +57,7 @@ import android.content.res.Configuration; import android.os.Binder; import android.os.Build; import android.os.IBinder; +import android.os.LocaleList; import android.os.Message; import android.os.Process; import android.os.RemoteException; @@ -817,10 +818,13 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio return false; } - void updateNightModeForAllActivities(int nightMode) { + // TODO(b/199277065): Re-assess how app-specific locales are applied based on UXR + // TODO(b/199277729): Consider whether we need to add special casing for edge cases like + // activity-embeddings etc. + void updateAppSpecificSettingsForAllActivities(Integer nightMode, LocaleList localesOverride) { for (int i = mActivities.size() - 1; i >= 0; --i) { final ActivityRecord r = mActivities.get(i); - if (r.setOverrideNightMode(nightMode) && r.mVisibleRequested) { + if (r.applyAppSpecificConfig(nightMode, localesOverride) && r.mVisibleRequested) { r.ensureActivityConfiguration(0 /* globalChanges */, true /* preserveWindow */); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java index 40a5a8159515..764f63dcd013 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java @@ -43,13 +43,17 @@ import static org.mockito.Mockito.when; import android.app.Activity; import android.app.ActivityManager; +import android.app.IApplicationThread; import android.app.PictureInPictureParams; import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.EnterPipRequestedItem; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Binder; import android.os.IBinder; +import android.os.LocaleList; import android.os.PowerManager; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; @@ -61,6 +65,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; import org.mockito.MockitoSession; import java.util.ArrayList; @@ -80,6 +85,9 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase { private final ArgumentCaptor<ClientTransaction> mClientTransactionCaptor = ArgumentCaptor.forClass(ClientTransaction.class); + private static final String DEFAULT_PACKAGE_NAME = "my.application.package"; + private static final int DEFAULT_USER_ID = 100; + @Before public void setUp() throws Exception { setBooted(mAtm); @@ -489,5 +497,269 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase { assertTrue(activity.supportsMultiWindow()); assertTrue(task.supportsMultiWindow()); } + + @Test + public void testPackageConfigUpdate_locales_successfullyApplied() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID); + mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID)); + + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(); + packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")).commit(); + + WindowProcessController wpcAfterConfigChange = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpcAfterConfigChange.getConfiguration().getLocales()); + assertFalse(wpcAfterConfigChange.getConfiguration().isNightModeActive()); + } + + @Test + public void testPackageConfigUpdate_nightMode_successfullyApplied() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID); + mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID)); + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(); + + packageConfigUpdater.setNightMode(Configuration.UI_MODE_NIGHT_YES).commit(); + + WindowProcessController wpcAfterConfigChange = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertTrue(wpcAfterConfigChange.getConfiguration().isNightModeActive()); + assertEquals(LocaleList.forLanguageTags("en-XC"), + wpcAfterConfigChange.getConfiguration().getLocales()); + } + + @Test + public void testPackageConfigUpdate_multipleLocaleUpdates_successfullyApplied() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID); + WindowProcessController wpc = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + mAtm.mProcessMap.put(Binder.getCallingPid(), wpc); + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(); + + packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")) + .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit(); + + WindowProcessController wpcAfterConfigChange1 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpcAfterConfigChange1.getConfiguration().getLocales()); + assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive()); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpc.getConfiguration().getLocales()); + + packageConfigUpdater.setLocales(LocaleList.forLanguageTags("ja-XC,en-XC")).commit(); + + WindowProcessController wpcAfterConfigChange2 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + + assertEquals(LocaleList.forLanguageTags("ja-XC,en-XC"), + wpcAfterConfigChange2.getConfiguration().getLocales()); + assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive()); + assertEquals(LocaleList.forLanguageTags("ja-XC,en-XC"), + wpc.getConfiguration().getLocales()); + } + + @Test + public void testPackageConfigUpdate_multipleNightModeUpdates_successfullyApplied() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID); + mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID)); + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(); + + packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")) + .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit(); + + WindowProcessController wpcAfterConfigChange1 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpcAfterConfigChange1.getConfiguration().getLocales()); + assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive()); + + packageConfigUpdater.setNightMode(Configuration.UI_MODE_NIGHT_NO).commit(); + + WindowProcessController wpcAfterConfigChange2 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpcAfterConfigChange2.getConfiguration().getLocales()); + assertFalse(wpcAfterConfigChange2.getConfiguration().isNightModeActive()); + } + + @Test + public void testPackageConfigUpdate_onPackageUninstall_configShouldNotApply() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID); + mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID)); + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(); + + packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")) + .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit(); + + WindowProcessController wpcAfterConfigChange1 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpcAfterConfigChange1.getConfiguration().getLocales()); + assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive()); + + mAtm.mInternal.onPackageUninstalled(DEFAULT_PACKAGE_NAME); + + WindowProcessController wpcAfterConfigChange2 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XC"), + wpcAfterConfigChange2.getConfiguration().getLocales()); + assertFalse(wpcAfterConfigChange2.getConfiguration().isNightModeActive()); + } + + @Test + public void testPackageConfigUpdate_LocalesEmptyAndNightModeUndefined_configShouldNotApply() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID); + WindowProcessController wpc = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + mAtm.mProcessMap.put(Binder.getCallingPid(), wpc); + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(); + + packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")) + .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit(); + WindowProcessController wpcAfterConfigChange1 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpcAfterConfigChange1.getConfiguration().getLocales()); + assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive()); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpc.getConfiguration().getLocales()); + + packageConfigUpdater.setLocales(LocaleList.getEmptyLocaleList()) + .setNightMode(Configuration.UI_MODE_NIGHT_UNDEFINED).commit(); + + WindowProcessController wpcAfterConfigChange2 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XC"), + wpcAfterConfigChange2.getConfiguration().getLocales()); + assertFalse(wpcAfterConfigChange2.getConfiguration().isNightModeActive()); + assertEquals(LocaleList.forLanguageTags("en-XC"), + wpc.getConfiguration().getLocales()); + } + + @Test + public void testPackageConfigUpdate_WhenUserRemoved_configShouldNotApply() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID); + mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID)); + + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(); + + packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")) + .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit(); + + WindowProcessController wpcAfterConfigChange1 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpcAfterConfigChange1.getConfiguration().getLocales()); + assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive()); + + mAtm.mInternal.removeUser(DEFAULT_USER_ID); + + WindowProcessController wpcAfterConfigChange2 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XC"), + wpcAfterConfigChange2.getConfiguration().getLocales()); + assertFalse(wpcAfterConfigChange2.getConfiguration().isNightModeActive()); + } + + @Test + public void testPackageConfigUpdate_setLocaleListToEmpty_doesNotOverlayLocaleListInWpc() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID); + mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID)); + + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(); + + packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")) + .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit(); + + WindowProcessController wpcAfterConfigChange1 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpcAfterConfigChange1.getConfiguration().getLocales()); + assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive()); + + packageConfigUpdater.setLocales(LocaleList.getEmptyLocaleList()).commit(); + + WindowProcessController wpcAfterConfigChange2 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XC"), + wpcAfterConfigChange2.getConfiguration().getLocales()); + assertTrue(wpcAfterConfigChange2.getConfiguration().isNightModeActive()); + } + + @Test + public void testPackageConfigUpdate_resetNightMode_doesNotOverrideNightModeInWpc() { + Configuration config = mAtm.getGlobalConfiguration(); + config.setLocales(LocaleList.forLanguageTags("en-XC")); + mAtm.updateGlobalConfigurationLocked(config, true, true, DEFAULT_USER_ID); + mAtm.mProcessMap.put(Binder.getCallingPid(), createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID)); + + ActivityTaskManagerInternal.PackageConfigurationUpdater packageConfigUpdater = + mAtm.mInternal.createPackageConfigurationUpdater(); + + packageConfigUpdater.setLocales(LocaleList.forLanguageTags("en-XA,ar-XB")) + .setNightMode(Configuration.UI_MODE_NIGHT_YES).commit(); + + WindowProcessController wpcAfterConfigChange1 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpcAfterConfigChange1.getConfiguration().getLocales()); + assertTrue(wpcAfterConfigChange1.getConfiguration().isNightModeActive()); + + packageConfigUpdater.setNightMode(Configuration.UI_MODE_NIGHT_UNDEFINED).commit(); + + WindowProcessController wpcAfterConfigChange2 = createWindowProcessController( + DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID); + assertEquals(LocaleList.forLanguageTags("en-XA,ar-XB,en-XC"), + wpcAfterConfigChange2.getConfiguration().getLocales()); + assertFalse(wpcAfterConfigChange2.getConfiguration().isNightModeActive()); + } + + private WindowProcessController createWindowProcessController(String packageName, + int userId) { + WindowProcessListener mMockListener = Mockito.mock(WindowProcessListener.class); + ApplicationInfo info = mock(ApplicationInfo.class); + info.packageName = packageName; + WindowProcessController wpc = new WindowProcessController( + mAtm, info, packageName, 0, userId, null, mMockListener); + wpc.setThread(mock(IApplicationThread.class)); + return wpc; + } + } + + diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java index d3f2d1407a46..c56b6141a652 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java @@ -34,6 +34,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -49,6 +50,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.ServiceInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.LocaleList; import android.platform.test.annotations.Presubmit; import org.junit.Before; @@ -371,8 +373,9 @@ public class WindowProcessControllerTests extends WindowTestsBase { public void testTopActivityUiModeChangeScheduleConfigChange() { final ActivityRecord activity = createActivityRecord(mWpc); activity.mVisibleRequested = true; - doReturn(true).when(activity).setOverrideNightMode(anyInt()); - mWpc.updateNightModeForAllActivities(Configuration.UI_MODE_NIGHT_YES); + doReturn(true).when(activity).applyAppSpecificConfig(anyInt(), any()); + mWpc.updateAppSpecificSettingsForAllActivities(Configuration.UI_MODE_NIGHT_YES, + LocaleList.forLanguageTags("en-XA")); verify(activity).ensureActivityConfiguration(anyInt(), anyBoolean()); } |