diff options
67 files changed, 2226 insertions, 691 deletions
diff --git a/core/java/android/app/ambientcontext/AmbientContextManager.java b/core/java/android/app/ambientcontext/AmbientContextManager.java index 7f913e798bc9..dd1dd0c75f76 100644 --- a/core/java/android/app/ambientcontext/AmbientContextManager.java +++ b/core/java/android/app/ambientcontext/AmbientContextManager.java @@ -153,7 +153,7 @@ public final class AmbientContextManager { * eventTypes.add(AmbientContextEvent.EVENT_SNORE); * * // Create Consumer - * Consumer<Integer> statusConsumer = response -> { + * Consumer<Integer> statusConsumer = status -> { * int status = status.getStatusCode(); * if (status == AmbientContextManager.STATUS_SUCCESS) { * // Show user it's enabled diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index e0e41d07d973..976a3e4d4cde 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -58,6 +58,12 @@ public class FeatureFlagUtils { public static final String SETTINGS_APP_LOCALE_OPT_IN_ENABLED = "settings_app_locale_opt_in_enabled"; + /** + * Launch the Volume panel in SystemUI. + * @hide + */ + public static final String SETTINGS_VOLUME_PANEL_IN_SYSTEMUI = + "settings_volume_panel_in_systemui"; /** @hide */ public static final String SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS = @@ -105,6 +111,7 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_SUPPORT_LARGE_SCREEN, "true"); DEFAULT_FLAGS.put("settings_search_always_expand", "true"); DEFAULT_FLAGS.put(SETTINGS_APP_LOCALE_OPT_IN_ENABLED, "true"); + DEFAULT_FLAGS.put(SETTINGS_VOLUME_PANEL_IN_SYSTEMUI, "false"); DEFAULT_FLAGS.put(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS, "true"); DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "true"); DEFAULT_FLAGS.put(SETTINGS_HIDE_SECOND_LAYER_PAGE_NAVIGATE_UP_BUTTON_IN_TWO_PANE, "true"); diff --git a/core/java/com/android/internal/widget/LocalImageResolver.java b/core/java/com/android/internal/widget/LocalImageResolver.java index b11ea2961c17..9ef7ce38fc09 100644 --- a/core/java/com/android/internal/widget/LocalImageResolver.java +++ b/core/java/com/android/internal/widget/LocalImageResolver.java @@ -19,6 +19,8 @@ package com.android.internal.widget; import android.annotation.DrawableRes; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.ImageDecoder; @@ -109,13 +111,13 @@ public class LocalImageResolver { } break; case Icon.TYPE_RESOURCE: - if (!(TextUtils.isEmpty(icon.getResPackage()) - || context.getPackageName().equals(icon.getResPackage()))) { - // We can't properly resolve icons from other packages here, so fall back. + Resources res = resolveResourcesForIcon(context, icon); + if (res == null) { + // We couldn't resolve resources properly, fall back to icon loading. return icon.loadDrawable(context); } - Drawable result = resolveImage(icon.getResId(), context, maxWidth, maxHeight); + Drawable result = resolveImage(res, icon.getResId(), maxWidth, maxHeight); if (result != null) { return tintDrawable(icon, result); } @@ -159,6 +161,13 @@ public class LocalImageResolver { } @Nullable + private static Drawable resolveImage(Resources res, @DrawableRes int resId, int maxWidth, + int maxHeight) { + final ImageDecoder.Source source = ImageDecoder.createSource(res, resId); + return resolveImage(source, maxWidth, maxHeight); + } + + @Nullable private static Drawable resolveBitmapImage(Icon icon, Context context, int maxWidth, int maxHeight) { @@ -259,4 +268,52 @@ public class LocalImageResolver { } return icon.getUri(); } + + /** + * Resolves the correct resources package for a given Icon - it may come from another + * package. + * + * @see Icon#loadDrawableInner(Context) + * @hide + * + * @return resources instance if the operation succeeded, null otherwise + */ + @Nullable + @VisibleForTesting + public static Resources resolveResourcesForIcon(Context context, Icon icon) { + if (icon.getType() != Icon.TYPE_RESOURCE) { + return null; + } + + // Icons cache resolved resources, use cache if available. + Resources res = icon.getResources(); + if (res != null) { + return res; + } + + String resPackage = icon.getResPackage(); + // No package means we try to use current context. + if (TextUtils.isEmpty(resPackage) || context.getPackageName().equals(resPackage)) { + return context.getResources(); + } + + if ("android".equals(resPackage)) { + return Resources.getSystem(); + } + + final PackageManager pm = context.getPackageManager(); + try { + ApplicationInfo ai = pm.getApplicationInfo(resPackage, + PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.GET_SHARED_LIBRARY_FILES); + if (ai != null) { + return pm.getResourcesForApplication(ai); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, String.format("Unable to resolve package %s for icon %s", resPackage, icon)); + return null; + } + + return null; + } } diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 88206270ca7f..8152b79c8659 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1565,7 +1565,8 @@ <bool name="config_enableIdleScreenBrightnessMode">false</bool> <!-- Array of desired screen brightness in nits corresponding to the lux values - in the config_autoBrightnessLevels array. The display brightness is defined as the measured + in the config_autoBrightnessLevels array. As with config_screenBrightnessMinimumNits and + config_screenBrightnessMaximumNits, the display brightness is defined as the measured brightness of an all-white image. If this is defined then: @@ -1586,7 +1587,7 @@ <array name="config_autoBrightnessDisplayValuesNitsIdle"> </array> - <!-- Array of output values for button backlight corresponding to the lux values + <!-- Array of output values for button backlight corresponding to the luX values in the config_autoBrightnessLevels array. This array should have size one greater than the size of the config_autoBrightnessLevels array. The brightness values must be between 0 and 255 and be non-decreasing. diff --git a/core/tests/coretests/src/com/android/internal/widget/LocalImageResolverTest.java b/core/tests/coretests/src/com/android/internal/widget/LocalImageResolverTest.java index 0cee526651a6..271a20b71106 100644 --- a/core/tests/coretests/src/com/android/internal/widget/LocalImageResolverTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/LocalImageResolverTest.java @@ -17,6 +17,8 @@ package com.android.internal.widget; import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; import android.graphics.BitmapFactory; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; @@ -279,4 +281,49 @@ public class LocalImageResolverTest { // This drawable must not be loaded - if it was, the code ignored the package specification. assertThat(d).isNull(); } + + @Test + public void resolveResourcesForIcon_notAResourceIcon_returnsNull() { + Icon icon = Icon.createWithContentUri(Uri.parse("some_uri")); + assertThat(LocalImageResolver.resolveResourcesForIcon(mContext, icon)).isNull(); + } + + @Test + public void resolveResourcesForIcon_localPackageIcon_returnsPackageResources() { + Icon icon = Icon.createWithResource(mContext, R.drawable.test32x24); + assertThat(LocalImageResolver.resolveResourcesForIcon(mContext, icon)) + .isSameInstanceAs(mContext.getResources()); + } + + @Test + public void resolveResourcesForIcon_iconWithoutPackageSpecificed_returnsPackageResources() { + Icon icon = Icon.createWithResource("", R.drawable.test32x24); + assertThat(LocalImageResolver.resolveResourcesForIcon(mContext, icon)) + .isSameInstanceAs(mContext.getResources()); + } + + @Test + public void resolveResourcesForIcon_systemPackageSpecified_returnsSystemPackage() { + Icon icon = Icon.createWithResource("android", R.drawable.test32x24); + assertThat(LocalImageResolver.resolveResourcesForIcon(mContext, icon)).isSameInstanceAs( + Resources.getSystem()); + } + + @Test + public void resolveResourcesForIcon_differentPackageSpecified_returnsPackageResources() throws + PackageManager.NameNotFoundException { + String pkg = "com.android.settings"; + Resources res = mContext.getPackageManager().getResourcesForApplication(pkg); + int resId = res.getIdentifier("ic_android", "drawable", pkg); + Icon icon = Icon.createWithResource(pkg, resId); + + assertThat(LocalImageResolver.resolveResourcesForIcon(mContext, icon).getDrawable(resId, + mContext.getTheme())).isNotNull(); + } + + @Test + public void resolveResourcesForIcon_invalidPackageSpecified_returnsNull() { + Icon icon = Icon.createWithResource("invalid.package", R.drawable.test32x24); + assertThat(LocalImageResolver.resolveResourcesForIcon(mContext, icon)).isNull(); + } } diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index f030d80a3533..e0e13f59b706 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -81,5 +81,6 @@ <permission name="android.permission.READ_DEVICE_CONFIG" /> <permission name="android.permission.READ_SAFETY_CENTER_STATUS" /> <permission name="android.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS" /> + <permission name="android.permission.READ_SEARCH_INDEXABLES" /> </privapp-permissions> </permissions> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java index 0d75bc451b72..f1465f421c5b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java @@ -105,8 +105,8 @@ public class FullscreenTaskListener<T extends AutoCloseable> state.mTaskInfo = taskInfo; mTasks.put(taskInfo.taskId, state); - updateRecentsForVisibleFullscreenTask(taskInfo); if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + updateRecentsForVisibleFullscreenTask(taskInfo); if (shouldShowWindowDecor(taskInfo) && mWindowDecorViewModelOptional.isPresent()) { SurfaceControl.Transaction t = new SurfaceControl.Transaction(); state.mWindowDecoration = @@ -135,8 +135,8 @@ public class FullscreenTaskListener<T extends AutoCloseable> mWindowDecorViewModelOptional.get().onTaskInfoChanged( state.mTaskInfo, state.mWindowDecoration); } - updateRecentsForVisibleFullscreenTask(taskInfo); if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + updateRecentsForVisibleFullscreenTask(taskInfo); final Point positionInParent = state.mTaskInfo.positionInParent; if (!oldPositionInParent.equals(state.mTaskInfo.positionInParent)) { diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 78dea891bc12..abcd65b5071a 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -85,7 +85,6 @@ <uses-permission android:name="android.permission.CONTROL_VPN" /> <uses-permission android:name="android.permission.PEERS_MAC_ADDRESS"/> <uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"/> - <uses-permission android:name="android.permission.NETWORK_STACK"/> <!-- Physical hardware --> <uses-permission android:name="android.permission.MANAGE_USB" /> <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" /> @@ -152,6 +151,9 @@ <uses-permission android:name="android.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS" /> <uses-permission android:name="android.permission.GET_RUNTIME_PERMISSIONS" /> + <!-- For auto-grant the access to the Settings' slice preferences, e.g. volume slices. --> + <uses-permission android:name="android.permission.READ_SEARCH_INDEXABLES" /> + <!-- Needed for WallpaperManager.clear in ImageWallpaper.updateWallpaperLocked --> <uses-permission android:name="android.permission.SET_WALLPAPER"/> @@ -956,5 +958,13 @@ </intent-filter> </receiver> + <receiver android:name=".volume.VolumePanelDialogReceiver" + android:exported="true"> + <intent-filter> + <action android:name="android.settings.panel.action.VOLUME" /> + <action android:name="com.android.systemui.action.LAUNCH_VOLUME_PANEL_DIALOG" /> + <action android:name="com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" /> + </intent-filter> + </receiver> </application> </manifest> diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/media_ttt_chip.xml index d88680669fe0..ae8e38e2634b 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip.xml @@ -16,7 +16,7 @@ <!-- Wrap in a frame layout so that we can update the margins on the inner layout. (Since this view is the root view of a window, we cannot change the root view's margins.) --> <!-- Alphas start as 0 because the view will be animated in. --> -<FrameLayout +<com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/media_ttt_sender_chip" @@ -97,4 +97,4 @@ /> </LinearLayout> -</FrameLayout> +</com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView> diff --git a/packages/SystemUI/res/layout/volume_panel_dialog.xml b/packages/SystemUI/res/layout/volume_panel_dialog.xml new file mode 100644 index 000000000000..99a1b5cc19dd --- /dev/null +++ b/packages/SystemUI/res/layout/volume_panel_dialog.xml @@ -0,0 +1,101 @@ +<?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. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/volume_panel_dialog" + android:layout_width="@dimen/large_dialog_width" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/Widget.SliceView.Panel" + android:gravity="center_vertical|center_horizontal" + android:layout_marginTop="@dimen/dialog_top_padding" + android:layout_marginBottom="@dimen/dialog_bottom_padding" + android:orientation="vertical"> + + <TextView + android:id="@+id/volume_panel_dialog_title" + android:ellipsize="end" + android:gravity="center_vertical|center_horizontal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/sound_settings" + android:textAppearance="@style/TextAppearance.Dialog.Title"/> + </LinearLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/volume_panel_parent_layout" + android:scrollbars="vertical" + android:layout_width="match_parent" + android:layout_height="0dp" + android:minHeight="304dp" + android:layout_weight="1" + android:overScrollMode="never"/> + + <LinearLayout + android:id="@+id/button_layout" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/dialog_button_vertical_padding" + android:layout_marginStart="@dimen/dialog_side_padding" + android:layout_marginEnd="@dimen/dialog_side_padding" + android:layout_marginBottom="@dimen/dialog_bottom_padding" + android:baselineAligned="false" + android:clickable="false" + android:focusable="false"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_gravity="start|center_vertical" + android:orientation="vertical"> + <Button + android:id="@+id/settings_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/volume_panel_dialog_settings_button" + android:ellipsize="end" + android:maxLines="1" + style="@style/Widget.Dialog.Button.BorderButton" + android:clickable="true" + android:focusable="true"/> + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/dialog_button_horizontal_padding" + android:layout_gravity="end|center_vertical"> + <Button + android:id="@+id/done_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/inline_done_button" + style="@style/Widget.Dialog.Button" + android:maxLines="1" + android:ellipsize="end" + android:clickable="true" + android:focusable="true"/> + </LinearLayout> + </LinearLayout> +</LinearLayout> diff --git a/packages/SystemUI/res/layout/volume_panel_slice_slider_row.xml b/packages/SystemUI/res/layout/volume_panel_slice_slider_row.xml new file mode 100644 index 000000000000..d1303ed88964 --- /dev/null +++ b/packages/SystemUI/res/layout/volume_panel_slice_slider_row.xml @@ -0,0 +1,31 @@ +<?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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/slice_slider_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <androidx.slice.widget.SliceView + android:id="@+id/slice_view" + style="@style/Widget.SliceView.Panel.Slider" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingVertical="@dimen/volume_panel_slice_vertical_padding" + android:paddingHorizontal="@dimen/volume_panel_slice_horizontal_padding"/> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index c9776dd58788..98202371d98f 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -479,6 +479,10 @@ <dimen name="volume_tool_tip_arrow_corner_radius">2dp</dimen> + <!-- Volume panel slices dimensions --> + <dimen name="volume_panel_slice_vertical_padding">8dp</dimen> + <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen> + <!-- Size of each item in the ringer selector drawer. --> <dimen name="volume_ringer_drawer_item_size">42dp</dimen> <dimen name="volume_ringer_drawer_item_size_half">21dp</dimen> @@ -1178,7 +1182,6 @@ <item name="shutdown_scrim_behind_alpha" format="float" type="dimen">0.95</item> <!-- Output switcher panel related dimensions --> - <dimen name="media_output_dialog_list_margin">12dp</dimen> <dimen name="media_output_dialog_list_max_height">355dp</dimen> <dimen name="media_output_dialog_header_album_icon_size">72dp</dimen> <dimen name="media_output_dialog_header_back_icon_size">32dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index e4fefc7c6705..2d0fa53b1f37 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1139,6 +1139,11 @@ <!-- Content description for accessibility: Hint if click will disable. [CHAR LIMIT=NONE] --> <string name="volume_odi_captions_hint_disable">disable</string> + <!-- Sound and vibration settings dialog title. [CHAR LIMIT=30] --> + <string name="sound_settings">Sound & vibration</string> + <!-- Label for button to go to sound settings screen [CHAR_LIMIT=30] --> + <string name="volume_panel_dialog_settings_button">Settings</string> + <!-- content description for audio output chooser [CHAR LIMIT=NONE]--> <!-- Screen pinning dialog title. --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 112d903c609c..6b2ff376d2c6 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -928,6 +928,10 @@ <item name="rowStyle">@style/SliceRow</item> </style> + <style name="Widget.SliceView.Panel.Slider"> + <item name="rowStyle">@style/SliceRow.Slider</item> + </style> + <style name="SliceRow"> <!-- 2dp start padding for the start icon --> <item name="titleItemStartPadding">2dp</item> @@ -949,6 +953,26 @@ <item name="actionDividerHeight">32dp</item> </style> + <style name="SliceRow.Slider"> + <!-- Padding between content and the start icon is 5dp --> + <item name="contentStartPadding">5dp</item> + <item name="contentEndPadding">0dp</item> + + <!-- 0dp start padding for the end item --> + <item name="endItemStartPadding">0dp</item> + <!-- 8dp end padding for the end item --> + <item name="endItemEndPadding">8dp</item> + + <item name="titleSize">20sp</item> + <!-- Align text with slider --> + <item name="titleStartPadding">11dp</item> + <item name="subContentStartPadding">11dp</item> + + <!-- Padding for indeterminate progress bar --> + <item name="progressBarStartPadding">12dp</item> + <item name="progressBarEndPadding">16dp</item> + </style> + <style name="TextAppearance.Dialog.Title" parent="@android:style/TextAppearance.DeviceDefault.Large"> <item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textSize">24sp</item> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index e1957c09fef5..93175e19a287 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -418,6 +418,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, state); getCurrentSecurityController().onResume(reason); + updateSideFpsVisibility(); } mView.onResume( mSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser()), diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt new file mode 100644 index 000000000000..109be40ce10f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt @@ -0,0 +1,67 @@ +package com.android.systemui + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FlagListenable +import com.android.systemui.flags.Flags +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext + +@SysUISingleton +class ChooserSelector @Inject constructor( + context: Context, + private val featureFlags: FeatureFlags, + @Application private val coroutineScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher +) : CoreStartable(context) { + + private val packageManager = context.packageManager + private val chooserComponent = ComponentName.unflattenFromString( + context.resources.getString(ChooserSelectorResourceHelper.CONFIG_CHOOSER_ACTIVITY)) + + override fun start() { + coroutineScope.launch { + val listener = FlagListenable.Listener { event -> + if (event.flagId == Flags.CHOOSER_UNBUNDLED.id) { + launch { updateUnbundledChooserEnabled() } + event.requestNoRestart() + } + } + featureFlags.addListener(Flags.CHOOSER_UNBUNDLED, listener) + updateUnbundledChooserEnabled() + + awaitCancellationAndThen { featureFlags.removeListener(listener) } + } + } + + private suspend fun updateUnbundledChooserEnabled() { + setUnbundledChooserEnabled(withContext(bgDispatcher) { + featureFlags.isEnabled(Flags.CHOOSER_UNBUNDLED) + }) + } + + private fun setUnbundledChooserEnabled(enabled: Boolean) { + val newState = if (enabled) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + packageManager.setComponentEnabledSetting(chooserComponent, newState, /* flags = */ 0) + } + + suspend inline fun awaitCancellation(): Nothing = suspendCancellableCoroutine { } + suspend inline fun awaitCancellationAndThen(block: () -> Unit): Nothing = try { + awaitCancellation() + } finally { + block() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java b/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java new file mode 100644 index 000000000000..7a2de7b6a78d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java @@ -0,0 +1,31 @@ +/* + * 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.systemui; + +import androidx.annotation.StringRes; + +import com.android.internal.R; + +/** Helper class for referencing resources */ +class ChooserSelectorResourceHelper { + + private ChooserSelectorResourceHelper() { + } + + @StringRes + static final int CONFIG_CHOOSER_ACTIVITY = R.string.config_chooserActivity; +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java index 8ba6f1c4a411..d60a22204b3d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java @@ -26,6 +26,7 @@ import com.android.systemui.people.widget.PeopleSpaceWidgetProvider; import com.android.systemui.screenshot.ActionProxyReceiver; import com.android.systemui.screenshot.DeleteScreenshotReceiver; import com.android.systemui.screenshot.SmartActionsReceiver; +import com.android.systemui.volume.VolumePanelDialogReceiver; import dagger.Binds; import dagger.Module; @@ -78,6 +79,15 @@ public abstract class DefaultBroadcastReceiverBinder { */ @Binds @IntoMap + @ClassKey(VolumePanelDialogReceiver.class) + public abstract BroadcastReceiver bindVolumePanelDialogReceiver( + VolumePanelDialogReceiver broadcastReceiver); + + /** + * + */ + @Binds + @IntoMap @ClassKey(PeopleSpaceWidgetPinnedReceiver.class) public abstract BroadcastReceiver bindPeopleSpaceWidgetPinnedReceiver( PeopleSpaceWidgetPinnedReceiver broadcastReceiver); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 6db3e82a77b0..8bb27a7bc217 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.dagger import com.android.keyguard.KeyguardBiometricLockoutLogger +import com.android.systemui.ChooserSelector import com.android.systemui.CoreStartable import com.android.systemui.LatencyTester import com.android.systemui.ScreenDecorations @@ -60,6 +61,12 @@ abstract class SystemUICoreStartableModule { @ClassKey(AuthController::class) abstract fun bindAuthController(service: AuthController): CoreStartable + /** Inject into ChooserCoreStartable. */ + @Binds + @IntoMap + @ClassKey(ChooserSelector::class) + abstract fun bindChooserSelector(sysui: ChooserSelector): CoreStartable + /** Inject into ClipboardListener. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index ccbadddddbc0..14823030b72e 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -253,6 +253,9 @@ public class Flags { // 1400 - columbus, b/242800729 public static final UnreleasedFlag QUICK_TAP_IN_PCC = new UnreleasedFlag(1400); + // 1500 - chooser + public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500); + // Pay no attention to the reflection behind the curtain. // ========================== Curtain ========================== // | | diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 012d76651b23..b02393b4f73a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -488,8 +488,8 @@ public class MediaControlPanel { TextView deviceName = mMediaViewHolder.getSeamlessText(); final MediaDeviceData device = data.getDevice(); - final boolean enabled; - final boolean seamlessDisabled; + final boolean isTapEnabled; + final boolean useDisabledAlpha; final int iconResource; CharSequence deviceString; if (showBroadcastButton) { @@ -499,21 +499,25 @@ public class MediaControlPanel { && TextUtils.equals(device.getName(), MediaDataUtils.getAppLabel(mContext, mPackageName, mContext.getString( R.string.bt_le_audio_broadcast_dialog_unknown_name))); - seamlessDisabled = !mIsCurrentBroadcastedApp; + useDisabledAlpha = !mIsCurrentBroadcastedApp; // Always be enabled if the broadcast button is shown - enabled = true; + isTapEnabled = true; + + // Defaults for broadcasting state deviceString = mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name); iconResource = R.drawable.settings_input_antenna; } else { // Disable clicking on output switcher for invalid devices and resumption controls - seamlessDisabled = (device != null && !device.getEnabled()) || data.getResumption(); - enabled = !seamlessDisabled; + useDisabledAlpha = (device != null && !device.getEnabled()) || data.getResumption(); + isTapEnabled = !useDisabledAlpha; + + // Defaults for non-broadcasting state deviceString = mContext.getString(R.string.media_seamless_other_device); iconResource = R.drawable.ic_media_home_devices; } - mMediaViewHolder.getSeamlessButton().setAlpha(seamlessDisabled ? DISABLED_ALPHA : 1.0f); - seamlessView.setEnabled(enabled); + mMediaViewHolder.getSeamlessButton().setAlpha(useDisabledAlpha ? DISABLED_ALPHA : 1.0f); + seamlessView.setEnabled(isTapEnabled); if (device != null) { Drawable icon = device.getIcon(); @@ -524,7 +528,9 @@ public class MediaControlPanel { } else { iconView.setImageDrawable(icon); } - deviceString = device.getName(); + if (device.getName() != null) { + deviceString = device.getName(); + } } else { // Set to default icon iconView.setImageResource(iconResource); diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index c8826757355a..fb37446c1cec 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -518,9 +518,20 @@ class MediaDataManager( } val actions = createActionsFromState(it.packageName, mediaControllerFactory.create(it.token), UserHandle(it.userId)) - val data = it.copy( - semanticActions = actions, - isPlaying = isPlayingState(state.state)) + + // Control buttons + // If flag is enabled and controller has a PlaybackState, + // create actions from session info + // otherwise, no need to update semantic actions. + val data = if (actions != null) { + it.copy( + semanticActions = actions, + isPlaying = isPlayingState(state.state)) + } else { + it.copy( + isPlaying = isPlayingState(state.state) + ) + } if (DEBUG) Log.d(TAG, "State updated outside of notification") onMediaDataLoaded(key, key, data) } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt index 83050503a18f..267c1f55a681 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt @@ -265,7 +265,6 @@ class MediaDeviceManager @Inject constructor( updateCurrent() } - override fun onBroadcastStarted(reason: Int, broadcastId: Int) { if (DEBUG) { Log.d(TAG, "onBroadcastStarted(), reason = $reason , broadcastId = $broadcastId") @@ -279,8 +278,10 @@ class MediaDeviceManager @Inject constructor( } } - override fun onBroadcastMetadataChanged(broadcastId: Int, - metadata: BluetoothLeBroadcastMetadata) { + override fun onBroadcastMetadataChanged( + broadcastId: Int, + metadata: BluetoothLeBroadcastMetadata + ) { if (DEBUG) { Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " + "metadata = $metadata") @@ -291,7 +292,6 @@ class MediaDeviceManager @Inject constructor( override fun onBroadcastStopped(reason: Int, broadcastId: Int) { if (DEBUG) { Log.d(TAG, "onBroadcastStopped(), reason = $reason , broadcastId = $broadcastId") - } updateCurrent() } @@ -344,7 +344,11 @@ class MediaDeviceManager @Inject constructor( // If we have a controller but get a null route, then don't trust the device val enabled = device != null && (controller == null || route != null) - val name = route?.name?.toString() ?: device?.name + val name = if (controller == null || route != null) { + route?.name?.toString() ?: device?.name + } else { + null + } current = MediaDeviceData(enabled, device?.iconWithoutBackground, name, id = device?.id, showBroadcastButton = false) } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index e360d10d9362..ee5956105b7b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -16,6 +16,7 @@ package com.android.systemui.media.dialog; +import android.annotation.DrawableRes; import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; @@ -42,9 +43,6 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { private static final String TAG = "MediaOutputAdapter"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private ViewGroup mConnectedItem; - private boolean mIncludeDynamicGroup; - public MediaOutputAdapter(MediaOutputController controller) { super(controller); setHasStableIds(true); @@ -102,141 +100,90 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) { super.onBind(device, topMargin, bottomMargin, position); boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice(); - final boolean currentlyConnected = !mIncludeDynamicGroup - && isCurrentlyConnected(device); + final boolean currentlyConnected = isCurrentlyConnected(device); boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE; - if (currentlyConnected) { - mConnectedItem = mContainerLayout; - } - mCheckBox.setVisibility(View.GONE); - mStatusIcon.setVisibility(View.GONE); - mEndTouchArea.setVisibility(View.GONE); - mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - mContainerLayout.setOnClickListener(null); - mContainerLayout.setContentDescription(null); - mTitleText.setTextColor(mController.getColorItemContent()); - mSubTitleText.setTextColor(mController.getColorItemContent()); - mTwoLineTitleText.setTextColor(mController.getColorItemContent()); - mSeekBar.getProgressDrawable().setColorFilter( - new PorterDuffColorFilter(mController.getColorSeekbarProgress(), - PorterDuff.Mode.SRC_IN)); if (mCurrentActivePosition == position) { mCurrentActivePosition = -1; } - if (mController.isTransferring()) { + if (mController.isAnyDeviceTransferring()) { if (device.getState() == MediaDeviceState.STATE_CONNECTING && !mController.hasAdjustVolumeUserRestriction()) { setUpDeviceIcon(device); - mProgressBar.getIndeterminateDrawable().setColorFilter( - new PorterDuffColorFilter( - mController.getColorItemContent(), - PorterDuff.Mode.SRC_IN)); - setSingleLineLayout(getItemTitle(device), true /* bFocused */, - false /* showSeekBar*/, - true /* showProgressBar */, false /* showStatus */); + updateProgressBarColor(); + setSingleLineLayout(getItemTitle(device), false /* showSeekBar*/, + true /* showProgressBar */, false /* showCheckBox */, + false /* showEndTouchArea */); } else { setUpDeviceIcon(device); - setSingleLineLayout(getItemTitle(device), false /* bFocused */); + setSingleLineLayout(getItemTitle(device)); } } else { // Set different layout for each device if (device.isMutingExpectedDevice() && !mController.isCurrentConnectedDeviceRemote()) { - mTitleIcon.setImageDrawable( - mContext.getDrawable(R.drawable.media_output_icon_volume)); - mTitleIcon.setColorFilter(mController.getColorItemContent()); - mTitleText.setTextColor(mController.getColorItemContent()); - setSingleLineLayout(getItemTitle(device), true /* bFocused */, - false /* showSeekBar */, - false /* showProgressBar */, false /* showStatus */); + updateTitleIcon(R.drawable.media_output_icon_volume, + mController.getColorItemContent()); initMutingExpectedDevice(); mCurrentActivePosition = position; - mContainerLayout.setOnClickListener(v -> onItemClick(v, device)); + updateContainerClickListener(v -> onItemClick(v, device)); + setSingleLineLayout(getItemTitle(device)); } else if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) { setUpDeviceIcon(device); - mStatusIcon.setImageDrawable( - mContext.getDrawable(R.drawable.media_output_status_failed)); - mStatusIcon.setColorFilter(mController.getColorItemContent()); - setTwoLineLayout(device, false /* bFocused */, - false /* showSeekBar */, false /* showProgressBar */, - true /* showSubtitle */, true /* showStatus */); + updateConnectionFailedStatusIcon(); mSubTitleText.setText(R.string.media_output_dialog_connect_failed); - mContainerLayout.setOnClickListener(v -> onItemClick(v, device)); + updateContainerClickListener(v -> onItemClick(v, device)); + setTwoLineLayout(device, false /* bFocused */, false /* showSeekBar */, + false /* showProgressBar */, true /* showSubtitle */, + true /* showStatus */); } else if (device.getState() == MediaDeviceState.STATE_GROUPING) { setUpDeviceIcon(device); - mProgressBar.getIndeterminateDrawable().setColorFilter( - new PorterDuffColorFilter( - mController.getColorItemContent(), - PorterDuff.Mode.SRC_IN)); - setSingleLineLayout(getItemTitle(device), true /* bFocused */, - false /* showSeekBar*/, - true /* showProgressBar */, false /* showStatus */); + updateProgressBarColor(); + setSingleLineLayout(getItemTitle(device), false /* showSeekBar*/, + true /* showProgressBar */, false /* showCheckBox */, + false /* showEndTouchArea */); } else if (mController.getSelectedMediaDevice().size() > 1 && isDeviceIncluded(mController.getSelectedMediaDevice(), device)) { boolean isDeviceDeselectable = isDeviceIncluded( mController.getDeselectableMediaDevice(), device); - mTitleText.setTextColor(mController.getColorItemContent()); - mTitleIcon.setImageDrawable( - mContext.getDrawable(R.drawable.media_output_icon_volume)); - mTitleIcon.setColorFilter(mController.getColorItemContent()); - setSingleLineLayout(getItemTitle(device), true /* bFocused */, - true /* showSeekBar */, - false /* showProgressBar */, false /* showStatus */); + updateTitleIcon(R.drawable.media_output_icon_volume, + mController.getColorItemContent()); + updateGroupableCheckBox(true, isDeviceDeselectable, device); + updateEndClickArea(device, isDeviceDeselectable); setUpContentDescriptionForView(mContainerLayout, false, device); - mCheckBox.setOnCheckedChangeListener(null); - mCheckBox.setVisibility(View.VISIBLE); - mCheckBox.setChecked(true); - mCheckBox.setOnCheckedChangeListener(isDeviceDeselectable - ? (buttonView, isChecked) -> onGroupActionTriggered(false, device) - : null); - mCheckBox.setEnabled(isDeviceDeselectable); - setCheckBoxColor(mCheckBox, mController.getColorItemContent()); + setSingleLineLayout(getItemTitle(device), true /* showSeekBar */, + false /* showProgressBar */, true /* showCheckBox */, + true /* showEndTouchArea */); initSeekbar(device, isCurrentSeekbarInvisible); - mEndTouchArea.setVisibility(View.VISIBLE); - mEndTouchArea.setOnClickListener(null); - mEndTouchArea.setOnClickListener( - isDeviceDeselectable ? (v) -> mCheckBox.performClick() : null); - mEndTouchArea.setImportantForAccessibility( - View.IMPORTANT_FOR_ACCESSIBILITY_YES); - setUpContentDescriptionForView(mEndTouchArea, true, device); } else if (!mController.hasAdjustVolumeUserRestriction() && currentlyConnected) { if (isMutingExpectedDeviceExist && !mController.isCurrentConnectedDeviceRemote()) { // mark as disconnected and set special click listener setUpDeviceIcon(device); - setSingleLineLayout(getItemTitle(device), false /* bFocused */); - mContainerLayout.setOnClickListener(v -> cancelMuteAwaitConnection()); + updateContainerClickListener(v -> cancelMuteAwaitConnection()); + setSingleLineLayout(getItemTitle(device)); } else { - mTitleIcon.setImageDrawable( - mContext.getDrawable(R.drawable.media_output_icon_volume)); - mTitleIcon.setColorFilter(mController.getColorItemContent()); - mTitleText.setTextColor(mController.getColorItemContent()); - setSingleLineLayout(getItemTitle(device), true /* bFocused */, - true /* showSeekBar */, - false /* showProgressBar */, false /* showStatus */); - initSeekbar(device, isCurrentSeekbarInvisible); + updateTitleIcon(R.drawable.media_output_icon_volume, + mController.getColorItemContent()); setUpContentDescriptionForView(mContainerLayout, false, device); mCurrentActivePosition = position; + setSingleLineLayout(getItemTitle(device), true /* showSeekBar */, + false /* showProgressBar */, false /* showCheckBox */, + false /* showEndTouchArea */); + initSeekbar(device, isCurrentSeekbarInvisible); } } else if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) { setUpDeviceIcon(device); - mCheckBox.setOnCheckedChangeListener(null); - mCheckBox.setVisibility(View.VISIBLE); - mCheckBox.setChecked(false); - mCheckBox.setOnCheckedChangeListener( - (buttonView, isChecked) -> onGroupActionTriggered(true, device)); - mEndTouchArea.setVisibility(View.VISIBLE); - mContainerLayout.setOnClickListener(v -> onGroupActionTriggered(true, device)); - setCheckBoxColor(mCheckBox, mController.getColorItemContent()); - setSingleLineLayout(getItemTitle(device), false /* bFocused */, - false /* showSeekBar */, - false /* showProgressBar */, false /* showStatus */); + updateGroupableCheckBox(false, true, device); + updateContainerClickListener(v -> onGroupActionTriggered(true, device)); + setSingleLineLayout(getItemTitle(device), false /* showSeekBar */, + false /* showProgressBar */, true /* showCheckBox */, + true /* showEndTouchArea */); } else { setUpDeviceIcon(device); - setSingleLineLayout(getItemTitle(device), false /* bFocused */); - mContainerLayout.setOnClickListener(v -> onItemClick(v, device)); + setSingleLineLayout(getItemTitle(device)); + updateContainerClickListener(v -> onItemClick(v, device)); } } } @@ -248,15 +195,56 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { ColorStateList(states, colors)); } + private void updateConnectionFailedStatusIcon() { + mStatusIcon.setImageDrawable( + mContext.getDrawable(R.drawable.media_output_status_failed)); + mStatusIcon.setColorFilter(mController.getColorItemContent()); + } + + private void updateProgressBarColor() { + mProgressBar.getIndeterminateDrawable().setColorFilter( + new PorterDuffColorFilter( + mController.getColorItemContent(), + PorterDuff.Mode.SRC_IN)); + } + + public void updateEndClickArea(MediaDevice device, boolean isDeviceDeselectable) { + mEndTouchArea.setOnClickListener(null); + mEndTouchArea.setOnClickListener( + isDeviceDeselectable ? (v) -> mCheckBox.performClick() : null); + mEndTouchArea.setImportantForAccessibility( + View.IMPORTANT_FOR_ACCESSIBILITY_YES); + setUpContentDescriptionForView(mEndTouchArea, true, device); + } + + private void updateGroupableCheckBox(boolean isSelected, boolean isGroupable, + MediaDevice device) { + mCheckBox.setOnCheckedChangeListener(null); + mCheckBox.setChecked(isSelected); + mCheckBox.setOnCheckedChangeListener( + isGroupable ? (buttonView, isChecked) -> onGroupActionTriggered(!isSelected, + device) : null); + mCheckBox.setEnabled(isGroupable); + setCheckBoxColor(mCheckBox, mController.getColorItemContent()); + } + + private void updateTitleIcon(@DrawableRes int id, int color) { + mTitleIcon.setImageDrawable(mContext.getDrawable(id)); + mTitleIcon.setColorFilter(color); + } + + private void updateContainerClickListener(View.OnClickListener listener) { + mContainerLayout.setOnClickListener(listener); + } + @Override void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) { if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) { mTitleText.setTextColor(mController.getColorItemContent()); mCheckBox.setVisibility(View.GONE); - setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new), - false /* bFocused */); - final Drawable d = mContext.getDrawable(R.drawable.ic_add); - mTitleIcon.setImageDrawable(d); + setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new)); + final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add); + mTitleIcon.setImageDrawable(addDrawable); mTitleIcon.setColorFilter(mController.getColorItemContent()); mContainerLayout.setOnClickListener(mController::launchBluetoothPairing); } @@ -273,7 +261,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } private void onItemClick(View view, MediaDevice device) { - if (mController.isTransferring()) { + if (mController.isAnyDeviceTransferring()) { return; } if (isCurrentlyConnected(device)) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index 3b4ca48046eb..3f7b2261ea52 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -63,8 +63,6 @@ public abstract class MediaOutputBaseAdapter extends protected final MediaOutputController mController; - private int mMargin; - Context mContext; View mHolderView; boolean mIsDragging; @@ -82,8 +80,6 @@ public abstract class MediaOutputBaseAdapter extends public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { mContext = viewGroup.getContext(); - mMargin = mContext.getResources().getDimensionPixelSize( - R.dimen.media_output_dialog_list_margin); mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item, viewGroup, false); @@ -168,16 +164,28 @@ public abstract class MediaOutputBaseAdapter extends void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) { mDeviceId = device.getId(); + mCheckBox.setVisibility(View.GONE); + mStatusIcon.setVisibility(View.GONE); + mEndTouchArea.setVisibility(View.GONE); + mEndTouchArea.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + mContainerLayout.setOnClickListener(null); + mContainerLayout.setContentDescription(null); + mTitleText.setTextColor(mController.getColorItemContent()); + mSubTitleText.setTextColor(mController.getColorItemContent()); + mTwoLineTitleText.setTextColor(mController.getColorItemContent()); + mSeekBar.getProgressDrawable().setColorFilter( + new PorterDuffColorFilter(mController.getColorSeekbarProgress(), + PorterDuff.Mode.SRC_IN)); } abstract void onBind(int customizedItem, boolean topMargin, boolean bottomMargin); - void setSingleLineLayout(CharSequence title, boolean bFocused) { - setSingleLineLayout(title, bFocused, false, false, false); + void setSingleLineLayout(CharSequence title) { + setSingleLineLayout(title, false, false, false, false); } - void setSingleLineLayout(CharSequence title, boolean bFocused, boolean showSeekBar, - boolean showProgressBar, boolean showStatus) { + void setSingleLineLayout(CharSequence title, boolean showSeekBar, + boolean showProgressBar, boolean showCheckBox, boolean showEndTouchArea) { mTwoLineLayout.setVisibility(View.GONE); boolean isActive = showSeekBar || showProgressBar; if (!mCornerAnimator.isRunning()) { @@ -188,10 +196,6 @@ public abstract class MediaOutputBaseAdapter extends .mutate() : mContext.getDrawable( R.drawable.media_output_item_background) .mutate(); - backgroundDrawable.setColorFilter(new PorterDuffColorFilter( - isActive ? mController.getColorConnectedItemBackground() - : mController.getColorItemBackground(), - PorterDuff.Mode.SRC_IN)); mItemLayout.setBackground(backgroundDrawable); if (showSeekBar) { final ClipDrawable clipDrawable = @@ -201,27 +205,21 @@ public abstract class MediaOutputBaseAdapter extends (GradientDrawable) clipDrawable.getDrawable(); progressDrawable.setCornerRadius(mController.getActiveRadius()); } - } else { - mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter( - isActive ? mController.getColorConnectedItemBackground() - : mController.getColorItemBackground(), - PorterDuff.Mode.SRC_IN)); } + mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter( + isActive ? mController.getColorConnectedItemBackground() + : mController.getColorItemBackground(), + PorterDuff.Mode.SRC_IN)); mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE); mSeekBar.setAlpha(1); mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE); if (!showSeekBar) { mSeekBar.resetVolume(); } - mStatusIcon.setVisibility(showStatus ? View.VISIBLE : View.GONE); mTitleText.setText(title); mTitleText.setVisibility(View.VISIBLE); - } - - void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar, - boolean showProgressBar, boolean showSubtitle) { - setTwoLineLayout(device, null, bFocused, showSeekBar, showProgressBar, showSubtitle, - false); + mCheckBox.setVisibility(showCheckBox ? View.VISIBLE : View.GONE); + mEndTouchArea.setVisibility(showEndTouchArea ? View.VISIBLE : View.GONE); } void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar, @@ -230,12 +228,6 @@ public abstract class MediaOutputBaseAdapter extends showStatus); } - void setTwoLineLayout(CharSequence title, boolean bFocused, boolean showSeekBar, - boolean showProgressBar, boolean showSubtitle) { - setTwoLineLayout(null, title, bFocused, showSeekBar, showProgressBar, showSubtitle, - false); - } - private void setTwoLineLayout(MediaDevice device, CharSequence title, boolean bFocused, boolean showSeekBar, boolean showProgressBar, boolean showSubtitle, boolean showStatus) { @@ -254,20 +246,11 @@ public abstract class MediaOutputBaseAdapter extends mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE); mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE); mTwoLineTitleText.setTranslationY(0); - if (device == null) { - mTwoLineTitleText.setText(title); - } else { - mTwoLineTitleText.setText(getItemTitle(device)); - } - - if (bFocused) { - mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString( - com.android.internal.R.string.config_headlineFontFamilyMedium), - Typeface.NORMAL)); - } else { - mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString( - com.android.internal.R.string.config_headlineFontFamily), Typeface.NORMAL)); - } + mTwoLineTitleText.setText(device == null ? title : getItemTitle(device)); + mTwoLineTitleText.setTypeface(Typeface.create(mContext.getString( + bFocused ? com.android.internal.R.string.config_headlineFontFamilyMedium + : com.android.internal.R.string.config_headlineFontFamily), + Typeface.NORMAL)); } void initSeekbar(MediaDevice device, boolean isCurrentSeekbarInvisible) { @@ -327,35 +310,6 @@ public abstract class MediaOutputBaseAdapter extends mItemLayout.setBackground(backgroundDrawable); } - void initSessionSeekbar() { - disableSeekBar(); - mSeekBar.setMax(mController.getSessionVolumeMax()); - mSeekBar.setMin(0); - final int currentVolume = mController.getSessionVolume(); - if (mSeekBar.getProgress() != currentVolume) { - mSeekBar.setProgress(currentVolume, true); - } - mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (!fromUser) { - return; - } - mController.adjustSessionVolume(progress); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - mIsDragging = true; - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - mIsDragging = false; - } - }); - } - private void animateCornerAndVolume(int fromProgress, int toProgress) { final GradientDrawable layoutBackgroundDrawable = (GradientDrawable) mItemLayout.getBackground(); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index dad6544a87ba..8dd843a6f921 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -421,7 +421,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, device.getId()); boolean isSelectedDeviceInGroup = getSelectedMediaDevice().size() > 1 && getSelectedMediaDevice().contains(device); - return (!hasAdjustVolumeUserRestriction() && isConnected && !isTransferring()) + return (!hasAdjustVolumeUserRestriction() && isConnected && !isAnyDeviceTransferring()) || isSelectedDeviceInGroup; } @@ -726,7 +726,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, UserHandle.of(UserHandle.myUserId())); } - boolean isTransferring() { + boolean isAnyDeviceTransferring() { synchronized (mMediaDevicesLock) { for (MediaDevice device : mMediaDevices) { if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) { diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt index a153cb6c0d31..f93c671f6740 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt @@ -26,6 +26,7 @@ import com.android.internal.logging.UiEventLogger import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.media.taptotransfer.common.DEFAULT_TIMEOUT_MILLIS +import com.android.systemui.plugins.FalsingManager /** * A class enumerating all the possible states of the media tap-to-transfer chip on the sender @@ -106,12 +107,15 @@ enum class ChipStateSender( controllerSender: MediaTttChipControllerSender, routeInfo: MediaRoute2Info, undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger + uiEventLogger: MediaTttSenderUiEventLogger, + falsingManager: FalsingManager, ): View.OnClickListener? { if (undoCallback == null) { return null } return View.OnClickListener { + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener + uiEventLogger.logUndoClicked( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED ) @@ -141,12 +145,15 @@ enum class ChipStateSender( controllerSender: MediaTttChipControllerSender, routeInfo: MediaRoute2Info, undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger + uiEventLogger: MediaTttSenderUiEventLogger, + falsingManager: FalsingManager, ): View.OnClickListener? { if (undoCallback == null) { return null } return View.OnClickListener { + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener + uiEventLogger.logUndoClicked( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED ) @@ -212,7 +219,8 @@ enum class ChipStateSender( controllerSender: MediaTttChipControllerSender, routeInfo: MediaRoute2Info, undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger + uiEventLogger: MediaTttSenderUiEventLogger, + falsingManager: FalsingManager, ): View.OnClickListener? = null companion object { diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt index 933548963390..5ad82fd9fd8f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt @@ -22,21 +22,25 @@ import android.media.MediaRoute2Info import android.os.PowerManager import android.util.Log import android.view.Gravity +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.widget.TextView import com.android.internal.statusbar.IUndoMediaTransferCallback +import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.animation.Interpolators import com.android.systemui.animation.ViewHierarchyAnimator +import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.taptotransfer.common.ChipInfoCommon import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCommon import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.common.MediaTttRemovalReason +import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor @@ -58,7 +62,9 @@ class MediaTttChipControllerSender @Inject constructor( accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, - private val uiEventLogger: MediaTttSenderUiEventLogger + private val uiEventLogger: MediaTttSenderUiEventLogger, + private val falsingManager: FalsingManager, + private val falsingCollector: FalsingCollector, ) : MediaTttChipControllerCommon<ChipSenderInfo>( context, logger, @@ -70,6 +76,9 @@ class MediaTttChipControllerSender @Inject constructor( powerManager, R.layout.media_ttt_chip, ) { + + private lateinit var parent: MediaTttChipRootView + override val windowLayoutParams = commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) } @@ -121,6 +130,15 @@ class MediaTttChipControllerSender @Inject constructor( val chipState = newChipInfo.state + // Detect falsing touches on the chip. + parent = currentChipView as MediaTttChipRootView + parent.touchHandler = object : Gefingerpoken { + override fun onTouchEvent(ev: MotionEvent?): Boolean { + falsingCollector.onTouchEvent(ev) + return false + } + } + // App icon val iconName = setIcon(currentChipView, newChipInfo.routeInfo.clientPackageName) @@ -136,7 +154,11 @@ class MediaTttChipControllerSender @Inject constructor( // Undo val undoView = currentChipView.requireViewById<View>(R.id.undo) val undoClickListener = chipState.undoClickListener( - this, newChipInfo.routeInfo, newChipInfo.undoCallback, uiEventLogger + this, + newChipInfo.routeInfo, + newChipInfo.undoCallback, + uiEventLogger, + falsingManager, ) undoView.setOnClickListener(undoClickListener) undoView.visibility = (undoClickListener != null).visibleIfTrue() diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt new file mode 100644 index 000000000000..3373159fba4e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt @@ -0,0 +1,38 @@ +/* + * 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.systemui.media.taptotransfer.sender + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.FrameLayout +import com.android.systemui.Gefingerpoken + +/** A simple subclass that allows for observing touch events on chip. */ +class MediaTttChipRootView( + context: Context, + attrs: AttributeSet? +) : FrameLayout(context, attrs) { + + /** Assign this field to observe touch events. */ + var touchHandler: Gefingerpoken? = null + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + touchHandler?.onTouchEvent(ev) + return super.dispatchTouchEvent(ev) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java index 3f931088ec83..5da480968b89 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileRevealController.java @@ -17,7 +17,14 @@ import java.util.Set; import javax.inject.Inject; -/** */ +/** + * Plays a animation to reveal newly added QS tiles. + * + * The aniumation is played when the user fully opens Quick Settings, and is only shown for + * <li> tiles added automatically (not through user customization) + * <li> tiles not have been revealed before (memoized via {@code QS_TILE_SPECS_REVEALED} + * preference) + */ public class QSTileRevealController { private static final long QS_REVEAL_TILES_DELAY = 500L; @@ -39,6 +46,7 @@ public class QSTileRevealController { }); } }; + QSTileRevealController(Context context, QSPanelController qsPanelController, PagedTileLayout pagedTileLayout, QSCustomizerController qsCustomizerController) { mContext = context; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt index a918e5d9e106..309059fdb9ad 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt @@ -17,6 +17,7 @@ package com.android.systemui.screenshot import android.graphics.Insets +import android.util.Log import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler import com.android.internal.util.ScreenshotHelper.ScreenshotRequest @@ -61,8 +62,9 @@ class RequestProcessor @Inject constructor( ) { val info = policy.findPrimaryContent(policy.getDefaultDisplayId()) + Log.d(TAG, "findPrimaryContent: $info") - result = if (policy.isManagedProfile(info.userId)) { + result = if (policy.isManagedProfile(info.user.identifier)) { val image = capture.captureTask(info.taskId) ?: error("Task snapshot returned a null Bitmap!") @@ -70,7 +72,7 @@ class RequestProcessor @Inject constructor( ScreenshotRequest( TAKE_SCREENSHOT_PROVIDED_IMAGE, request.source, HardwareBitmapBundler.hardwareBitmapToBundle(image), - info.bounds, Insets.NONE, info.taskId, info.userId, info.component + info.bounds, Insets.NONE, info.taskId, info.user.identifier, info.component ) } else { // Create a new request of the same type which includes the top component diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt index 3580010cc1e8..f73d2041af95 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt @@ -19,6 +19,7 @@ package com.android.systemui.screenshot import android.annotation.UserIdInt import android.content.ComponentName import android.graphics.Rect +import android.os.UserHandle import android.view.Display /** @@ -42,7 +43,7 @@ interface ScreenshotPolicy { data class DisplayContentInfo( val component: ComponentName, val bounds: Rect, - @UserIdInt val userId: Int, + val user: UserHandle, val taskId: Int, ) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt index ba809f676f1e..c2a50609b6a5 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt @@ -29,9 +29,11 @@ import android.content.Intent import android.graphics.Rect import android.os.Process import android.os.RemoteException +import android.os.UserHandle import android.os.UserManager import android.util.Log import android.view.Display.DEFAULT_DISPLAY +import com.android.internal.annotations.VisibleForTesting import com.android.internal.infra.ServiceConnector import com.android.systemui.SystemUIService import com.android.systemui.dagger.SysUISingleton @@ -45,21 +47,13 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext @SysUISingleton -internal class ScreenshotPolicyImpl @Inject constructor( +internal open class ScreenshotPolicyImpl @Inject constructor( context: Context, private val userMgr: UserManager, private val atmService: IActivityTaskManager, @Background val bgDispatcher: CoroutineDispatcher, ) : ScreenshotPolicy { - private val systemUiContent = - DisplayContentInfo( - ComponentName(context, SystemUIService::class.java), - Rect(), - ActivityTaskManager.INVALID_TASK_ID, - Process.myUserHandle().identifier, - ) - private val proxyConnector: ServiceConnector<IScreenshotProxy> = ServiceConnector.Impl( context, @@ -78,6 +72,9 @@ internal class ScreenshotPolicyImpl @Inject constructor( } private fun nonPipVisibleTask(info: RootTaskInfo): Boolean { + if (DEBUG) { + debugLogRootTaskInfo(info) + } return info.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED && info.isVisible && info.isRunning && @@ -99,58 +96,46 @@ internal class ScreenshotPolicyImpl @Inject constructor( } val taskInfoList = getAllRootTaskInfosOnDisplay(displayId) - if (DEBUG) { - debugLogRootTaskInfos(taskInfoList) - } // If no visible task is located, then report SystemUI as the foreground content val target = taskInfoList.firstOrNull(::nonPipVisibleTask) ?: return systemUiContent - - val topActivity: ComponentName = target.topActivity ?: error("should not be null") - val topChildTask = target.childTaskIds.size - 1 - val childTaskId = target.childTaskIds[topChildTask] - val childTaskUserId = target.childTaskUserIds[topChildTask] - val childTaskBounds = target.childTaskBounds[topChildTask] - - return DisplayContentInfo(topActivity, childTaskBounds, childTaskId, childTaskUserId) + return target.toDisplayContentInfo() } - private fun debugLogRootTaskInfos(taskInfoList: List<RootTaskInfo>) { - for (info in taskInfoList) { - Log.d( - TAG, - "[root task info] " + - "taskId=${info.taskId} " + - "parentTaskId=${info.parentTaskId} " + - "position=${info.position} " + - "positionInParent=${info.positionInParent} " + - "isVisible=${info.isVisible()} " + - "visible=${info.visible} " + - "isFocused=${info.isFocused} " + - "isSleeping=${info.isSleeping} " + - "isRunning=${info.isRunning} " + - "windowMode=${windowingModeToString(info.windowingMode)} " + - "activityType=${activityTypeToString(info.activityType)} " + - "topActivity=${info.topActivity} " + - "topActivityInfo=${info.topActivityInfo} " + - "numActivities=${info.numActivities} " + - "childTaskIds=${Arrays.toString(info.childTaskIds)} " + - "childUserIds=${Arrays.toString(info.childTaskUserIds)} " + - "childTaskBounds=${Arrays.toString(info.childTaskBounds)} " + - "childTaskNames=${Arrays.toString(info.childTaskNames)}" - ) - - for (j in 0 until info.childTaskIds.size) { - Log.d(TAG, " *** [$j] ******") - Log.d(TAG, " *** childTaskIds[$j]: ${info.childTaskIds[j]}") - Log.d(TAG, " *** childTaskUserIds[$j]: ${info.childTaskUserIds[j]}") - Log.d(TAG, " *** childTaskBounds[$j]: ${info.childTaskBounds[j]}") - Log.d(TAG, " *** childTaskNames[$j]: ${info.childTaskNames[j]}") - } + private fun debugLogRootTaskInfo(info: RootTaskInfo) { + Log.d(TAG, "RootTaskInfo={" + + "taskId=${info.taskId} " + + "parentTaskId=${info.parentTaskId} " + + "position=${info.position} " + + "positionInParent=${info.positionInParent} " + + "isVisible=${info.isVisible()} " + + "visible=${info.visible} " + + "isFocused=${info.isFocused} " + + "isSleeping=${info.isSleeping} " + + "isRunning=${info.isRunning} " + + "windowMode=${windowingModeToString(info.windowingMode)} " + + "activityType=${activityTypeToString(info.activityType)} " + + "topActivity=${info.topActivity} " + + "topActivityInfo=${info.topActivityInfo} " + + "numActivities=${info.numActivities} " + + "childTaskIds=${Arrays.toString(info.childTaskIds)} " + + "childUserIds=${Arrays.toString(info.childTaskUserIds)} " + + "childTaskBounds=${Arrays.toString(info.childTaskBounds)} " + + "childTaskNames=${Arrays.toString(info.childTaskNames)}" + + "}" + ) + + for (j in 0 until info.childTaskIds.size) { + Log.d(TAG, " *** [$j] ******") + Log.d(TAG, " *** childTaskIds[$j]: ${info.childTaskIds[j]}") + Log.d(TAG, " *** childTaskUserIds[$j]: ${info.childTaskUserIds[j]}") + Log.d(TAG, " *** childTaskBounds[$j]: ${info.childTaskBounds[j]}") + Log.d(TAG, " *** childTaskNames[$j]: ${info.childTaskNames[j]}") } } - private suspend fun getAllRootTaskInfosOnDisplay(displayId: Int): List<RootTaskInfo> = + @VisibleForTesting + open suspend fun getAllRootTaskInfosOnDisplay(displayId: Int): List<RootTaskInfo> = withContext(bgDispatcher) { try { atmService.getAllRootTaskInfosOnDisplay(displayId) @@ -160,7 +145,8 @@ internal class ScreenshotPolicyImpl @Inject constructor( } } - private suspend fun isNotificationShadeExpanded(): Boolean = suspendCoroutine { k -> + @VisibleForTesting + open suspend fun isNotificationShadeExpanded(): Boolean = suspendCoroutine { k -> proxyConnector .postForResult { it.isNotificationShadeExpanded } .whenComplete { expanded, error -> @@ -171,8 +157,30 @@ internal class ScreenshotPolicyImpl @Inject constructor( } } - companion object { - const val TAG: String = "ScreenshotPolicyImpl" - const val DEBUG: Boolean = false - } + @VisibleForTesting + internal val systemUiContent = + DisplayContentInfo( + ComponentName(context, SystemUIService::class.java), + Rect(), + Process.myUserHandle(), + ActivityTaskManager.INVALID_TASK_ID + ) +} + +private const val TAG: String = "ScreenshotPolicyImpl" +private const val DEBUG: Boolean = false + +@VisibleForTesting +internal fun RootTaskInfo.toDisplayContentInfo(): DisplayContentInfo { + val topActivity: ComponentName = topActivity ?: error("should not be null") + val topChildTask = childTaskIds.size - 1 + val childTaskId = childTaskIds[topChildTask] + val childTaskUserId = childTaskUserIds[topChildTask] + val childTaskBounds = childTaskBounds[topChildTask] + + return DisplayContentInfo( + topActivity, + childTaskBounds, + UserHandle.of(childTaskUserId), + childTaskId) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt index dbf4810b4fd7..126a986ee5f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -18,10 +18,8 @@ package com.android.systemui.statusbar.notification import android.animation.ObjectAnimator import android.util.FloatProperty -import com.android.systemui.Dumpable import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -34,20 +32,17 @@ import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener -import java.io.PrintWriter import javax.inject.Inject import kotlin.math.min @SysUISingleton class NotificationWakeUpCoordinator @Inject constructor( - dumpManager: DumpManager, private val mHeadsUpManager: HeadsUpManager, private val statusBarStateController: StatusBarStateController, private val bypassController: KeyguardBypassController, private val dozeParameters: DozeParameters, private val screenOffAnimationController: ScreenOffAnimationController -) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, PanelExpansionListener, - Dumpable { +) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, PanelExpansionListener { private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>( "notificationVisibility") { @@ -65,7 +60,6 @@ class NotificationWakeUpCoordinator @Inject constructor( private var mLinearDozeAmount: Float = 0.0f private var mDozeAmount: Float = 0.0f - private var mDozeAmountSource: String = "init" private var mNotificationVisibleAmount = 0.0f private var mNotificationsVisible = false private var mNotificationsVisibleForExpansion = false @@ -148,7 +142,6 @@ class NotificationWakeUpCoordinator @Inject constructor( } init { - dumpManager.registerDumpable(this) mHeadsUpManager.addListener(this) statusBarStateController.addCallback(this) addListener(object : WakeUpListener { @@ -255,14 +248,13 @@ class NotificationWakeUpCoordinator @Inject constructor( // Let's notify the scroller that an animation started notifyAnimationStart(mLinearDozeAmount == 1.0f) } - setDozeAmount(linear, eased, source = "StatusBar") + setDozeAmount(linear, eased) } - fun setDozeAmount(linear: Float, eased: Float, source: String) { + fun setDozeAmount(linear: Float, eased: Float) { val changed = linear != mLinearDozeAmount mLinearDozeAmount = linear mDozeAmount = eased - mDozeAmountSource = source mStackScrollerController.setDozeAmount(mDozeAmount) updateHideAmount() if (changed && linear == 0.0f) { @@ -279,7 +271,7 @@ class NotificationWakeUpCoordinator @Inject constructor( // undefined state, so it's an indication that we should do state cleanup. We override // the doze amount to 0f (not dozing) so that the notifications are no longer hidden. // See: UnlockedScreenOffAnimationController.onFinishedWakingUp() - setDozeAmount(0f, 0f, source = "Override: Shade->Shade (lock cancelled by unlock)") + setDozeAmount(0f, 0f) } if (overrideDozeAmountIfAnimatingScreenOff(mLinearDozeAmount)) { @@ -319,11 +311,12 @@ class NotificationWakeUpCoordinator @Inject constructor( */ private fun overrideDozeAmountIfBypass(): Boolean { if (bypassController.bypassEnabled) { - if (statusBarStateController.state == StatusBarState.KEYGUARD) { - setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)") - } else { - setDozeAmount(0f, 0f, source = "Override: bypass (shade)") + var amount = 1.0f + if (statusBarStateController.state == StatusBarState.SHADE || + statusBarStateController.state == StatusBarState.SHADE_LOCKED) { + amount = 0.0f } + setDozeAmount(amount, amount) return true } return false @@ -339,7 +332,7 @@ class NotificationWakeUpCoordinator @Inject constructor( */ private fun overrideDozeAmountIfAnimatingScreenOff(linearDozeAmount: Float): Boolean { if (screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()) { - setDozeAmount(1f, 1f, source = "Override: animating screen off") + setDozeAmount(1f, 1f) return true } @@ -433,24 +426,4 @@ class NotificationWakeUpCoordinator @Inject constructor( */ @JvmDefault fun onPulseExpansionChanged(expandingChanged: Boolean) {} } - - override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.println("mLinearDozeAmount: $mLinearDozeAmount") - pw.println("mDozeAmount: $mDozeAmount") - pw.println("mDozeAmountSource: $mDozeAmountSource") - pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount") - pw.println("mNotificationsVisible: $mNotificationsVisible") - pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion") - pw.println("mVisibilityAmount: $mVisibilityAmount") - pw.println("mLinearVisibilityAmount: $mLinearVisibilityAmount") - pw.println("pulseExpanding: $pulseExpanding") - pw.println("state: ${StatusBarState.toString(state)}") - pw.println("fullyAwake: $fullyAwake") - pw.println("wakingUp: $wakingUp") - pw.println("willWakeUp: $willWakeUp") - pw.println("collapsedEnoughToHide: $collapsedEnoughToHide") - pw.println("pulsing: $pulsing") - pw.println("notificationsFullyHidden: $notificationsFullyHidden") - pw.println("canShowPulsingHuns: $canShowPulsingHuns") - } -} +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index c94a915b2747..e6a3e74c1caf 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -48,7 +48,6 @@ import android.app.KeyguardManager; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.Configuration; @@ -248,6 +247,7 @@ public class VolumeDialogImpl implements VolumeDialog, private final ConfigurationController mConfigurationController; private final MediaOutputDialogFactory mMediaOutputDialogFactory; + private final VolumePanelFactory mVolumePanelFactory; private final ActivityStarter mActivityStarter; private boolean mShowing; @@ -279,6 +279,7 @@ public class VolumeDialogImpl implements VolumeDialog, DeviceProvisionedController deviceProvisionedController, ConfigurationController configurationController, MediaOutputDialogFactory mediaOutputDialogFactory, + VolumePanelFactory volumePanelFactory, ActivityStarter activityStarter, InteractionJankMonitor interactionJankMonitor) { mContext = @@ -290,6 +291,7 @@ public class VolumeDialogImpl implements VolumeDialog, mDeviceProvisionedController = deviceProvisionedController; mConfigurationController = configurationController; mMediaOutputDialogFactory = mediaOutputDialogFactory; + mVolumePanelFactory = volumePanelFactory; mActivityStarter = activityStarter; mShowActiveStreamOnly = showActiveStreamOnly(); mHasSeenODICaptionsTooltip = @@ -1043,10 +1045,9 @@ public class VolumeDialogImpl implements VolumeDialog, if (mSettingsIcon != null) { mSettingsIcon.setOnClickListener(v -> { Events.writeEvent(Events.EVENT_SETTINGS_CLICK); - Intent intent = new Intent(Settings.Panel.ACTION_VOLUME); dismissH(DISMISS_REASON_SETTINGS_CLICKED); mMediaOutputDialogFactory.dismiss(); - mActivityStarter.startActivity(intent, true /* dismissShade */); + mVolumePanelFactory.create(true /* aboveStatusBar */, null); }); } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialog.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialog.java new file mode 100644 index 000000000000..2c74fb911688 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialog.java @@ -0,0 +1,299 @@ +/* + * 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.systemui.volume; + +import android.bluetooth.BluetoothDevice; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.provider.SettingsSlicesContract; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.lifecycle.LiveData; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.slice.Slice; +import androidx.slice.SliceMetadata; +import androidx.slice.widget.EventInfo; +import androidx.slice.widget.SliceLiveData; + +import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.media.MediaOutputConstants; +import com.android.systemui.R; +import com.android.systemui.statusbar.phone.SystemUIDialog; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Visual presentation of the volume panel dialog. + */ +public class VolumePanelDialog extends SystemUIDialog implements LifecycleOwner { + private static final String TAG = "VolumePanelDialog"; + + private static final int DURATION_SLICE_BINDING_TIMEOUT_MS = 200; + private static final int DEFAULT_SLICE_SIZE = 4; + + private RecyclerView mVolumePanelSlices; + private VolumePanelSlicesAdapter mVolumePanelSlicesAdapter; + private final LifecycleRegistry mLifecycleRegistry; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final Map<Uri, LiveData<Slice>> mSliceLiveData = new LinkedHashMap<>(); + private final HashSet<Uri> mLoadedSlices = new HashSet<>(); + private boolean mSlicesReadyToLoad; + private LocalBluetoothProfileManager mProfileManager; + + public VolumePanelDialog(Context context, boolean aboveStatusBar) { + super(context); + mLifecycleRegistry = new LifecycleRegistry(this); + if (!aboveStatusBar) { + getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d(TAG, "onCreate"); + + View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.volume_panel_dialog, + null); + final Window window = getWindow(); + window.setContentView(dialogView); + + Button doneButton = dialogView.findViewById(R.id.done_button); + doneButton.setOnClickListener(v -> dismiss()); + Button settingsButton = dialogView.findViewById(R.id.settings_button); + settingsButton.setOnClickListener(v -> { + getContext().startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS).addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK)); + dismiss(); + }); + + LocalBluetoothManager localBluetoothManager = LocalBluetoothManager.getInstance( + getContext(), null); + if (localBluetoothManager != null) { + mProfileManager = localBluetoothManager.getProfileManager(); + } + + mVolumePanelSlices = dialogView.findViewById(R.id.volume_panel_parent_layout); + mVolumePanelSlices.setLayoutManager(new LinearLayoutManager(getContext())); + + loadAllSlices(); + + mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED); + } + + private void loadAllSlices() { + mSliceLiveData.clear(); + mLoadedSlices.clear(); + final List<Uri> sliceUris = getSlices(); + + for (Uri uri : sliceUris) { + final LiveData<Slice> sliceLiveData = SliceLiveData.fromUri(getContext(), uri, + (int type, Throwable source) -> { + if (!removeSliceLiveData(uri)) { + mLoadedSlices.add(uri); + } + }); + + // Add slice first to make it in order. Will remove it later if there's an error. + mSliceLiveData.put(uri, sliceLiveData); + + sliceLiveData.observe(this, slice -> { + if (mLoadedSlices.contains(uri)) { + return; + } + Log.d(TAG, "received slice: " + (slice == null ? null : slice.getUri())); + final SliceMetadata metadata = SliceMetadata.from(getContext(), slice); + if (slice == null || metadata.isErrorSlice()) { + if (!removeSliceLiveData(uri)) { + mLoadedSlices.add(uri); + } + } else if (metadata.getLoadingState() == SliceMetadata.LOADED_ALL) { + mLoadedSlices.add(uri); + } else { + mHandler.postDelayed(() -> { + mLoadedSlices.add(uri); + setupAdapterWhenReady(); + }, DURATION_SLICE_BINDING_TIMEOUT_MS); + } + + setupAdapterWhenReady(); + }); + } + } + + private void setupAdapterWhenReady() { + if (mLoadedSlices.size() == mSliceLiveData.size() && !mSlicesReadyToLoad) { + mSlicesReadyToLoad = true; + mVolumePanelSlicesAdapter = new VolumePanelSlicesAdapter(this, mSliceLiveData); + mVolumePanelSlicesAdapter.setOnSliceActionListener((eventInfo, sliceItem) -> { + if (eventInfo.actionType == EventInfo.ACTION_TYPE_SLIDER) { + return; + } + this.dismiss(); + }); + if (mSliceLiveData.size() < DEFAULT_SLICE_SIZE) { + mVolumePanelSlices.setMinimumHeight(0); + } + mVolumePanelSlices.setAdapter(mVolumePanelSlicesAdapter); + } + } + + private boolean removeSliceLiveData(Uri uri) { + boolean removed = false; + // Keeps observe media output slice + if (!uri.equals(MEDIA_OUTPUT_INDICATOR_SLICE_URI)) { + Log.d(TAG, "remove uri: " + uri); + removed = mSliceLiveData.remove(uri) != null; + if (mVolumePanelSlicesAdapter != null) { + mVolumePanelSlicesAdapter.updateDataSet(new ArrayList<>(mSliceLiveData.values())); + } + } + return removed; + } + + @Override + protected void onStart() { + super.onStart(); + Log.d(TAG, "onStart"); + mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED); + mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED); + } + + @Override + protected void onStop() { + super.onStop(); + Log.d(TAG, "onStop"); + mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED); + } + + private List<Uri> getSlices() { + final List<Uri> uris = new ArrayList<>(); + uris.add(REMOTE_MEDIA_SLICE_URI); + uris.add(VOLUME_MEDIA_URI); + Uri controlUri = getExtraControlUri(); + if (controlUri != null) { + Log.d(TAG, "add extra control slice"); + uris.add(controlUri); + } + uris.add(MEDIA_OUTPUT_INDICATOR_SLICE_URI); + uris.add(VOLUME_CALL_URI); + uris.add(VOLUME_RINGER_URI); + uris.add(VOLUME_ALARM_URI); + return uris; + } + + private static final String SETTINGS_SLICE_AUTHORITY = "com.android.settings.slices"; + private static final Uri REMOTE_MEDIA_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SETTINGS_SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(MediaOutputConstants.KEY_REMOTE_MEDIA) + .build(); + private static final Uri VOLUME_MEDIA_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SETTINGS_SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("media_volume") + .build(); + private static final Uri MEDIA_OUTPUT_INDICATOR_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SETTINGS_SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT) + .appendPath("media_output_indicator") + .build(); + private static final Uri VOLUME_CALL_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SETTINGS_SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("call_volume") + .build(); + private static final Uri VOLUME_RINGER_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SETTINGS_SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("ring_volume") + .build(); + private static final Uri VOLUME_ALARM_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SETTINGS_SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("alarm_volume") + .build(); + + private Uri getExtraControlUri() { + Uri controlUri = null; + final BluetoothDevice bluetoothDevice = findActiveDevice(); + if (bluetoothDevice != null) { + // The control slice width = dialog width - horizontal padding of two sides + final int dialogWidth = + getWindow().getWindowManager().getCurrentWindowMetrics().getBounds().width(); + final int controlSliceWidth = dialogWidth + - getContext().getResources().getDimensionPixelSize( + R.dimen.volume_panel_slice_horizontal_padding) * 2; + final String uri = BluetoothUtils.getControlUriMetaData(bluetoothDevice); + if (!TextUtils.isEmpty(uri)) { + try { + controlUri = Uri.parse(uri + controlSliceWidth); + } catch (NullPointerException exception) { + Log.d(TAG, "unable to parse extra control uri"); + controlUri = null; + } + } + } + return controlUri; + } + + private BluetoothDevice findActiveDevice() { + if (mProfileManager != null) { + final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); + if (a2dpProfile != null) { + return a2dpProfile.getActiveDevice(); + } + } + return null; + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return mLifecycleRegistry; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt new file mode 100644 index 000000000000..f11d5d18ac84 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt @@ -0,0 +1,46 @@ +/* + * 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.systemui.volume + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.text.TextUtils +import android.util.Log +import javax.inject.Inject + +private const val TAG = "VolumePanelDialogReceiver" +private const val LAUNCH_ACTION = "com.android.systemui.action.LAUNCH_VOLUME_PANEL_DIALOG" +private const val DISMISS_ACTION = "com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" + +/** + * BroadcastReceiver for handling volume panel dialog intent + */ +class VolumePanelDialogReceiver @Inject constructor( + private val volumePanelFactory: VolumePanelFactory +) : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Log.d(TAG, "onReceive intent" + intent.action) + if (TextUtils.equals(LAUNCH_ACTION, intent.action) || + TextUtils.equals(Settings.Panel.ACTION_VOLUME, intent.action)) { + volumePanelFactory.create(true, null) + } else if (TextUtils.equals(DISMISS_ACTION, intent.action)) { + volumePanelFactory.dismiss() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelFactory.kt new file mode 100644 index 000000000000..c2fafbf9f55b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelFactory.kt @@ -0,0 +1,67 @@ +/* + * 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.systemui.volume + +import android.content.Context +import android.util.Log +import android.view.View +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +private const val TAG = "VolumePanelFactory" +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + +/** + * Factory to create [VolumePanelDialog] objects. This is the dialog that allows the user to adjust + * multiple streams with sliders. + */ +@SysUISingleton +class VolumePanelFactory @Inject constructor( + private val context: Context, + private val dialogLaunchAnimator: DialogLaunchAnimator +) { + companion object { + var volumePanelDialog: VolumePanelDialog? = null + } + + /** Creates a [VolumePanelDialog]. The dialog will be animated from [view] if it is not null. */ + fun create(aboveStatusBar: Boolean, view: View? = null) { + if (volumePanelDialog?.isShowing == true) { + return + } + + val dialog = VolumePanelDialog(context, aboveStatusBar) + volumePanelDialog = dialog + + // Show the dialog. + if (view != null) { + dialogLaunchAnimator.showFromView(dialog, view, animateBackgroundBoundsChange = true) + } else { + dialog.show() + } + } + + /** Dismiss [VolumePanelDialog] if exist. */ + fun dismiss() { + if (DEBUG) { + Log.d(TAG, "dismiss dialog") + } + volumePanelDialog?.dismiss() + volumePanelDialog = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelSlicesAdapter.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelSlicesAdapter.java new file mode 100644 index 000000000000..23714021a2cc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelSlicesAdapter.java @@ -0,0 +1,137 @@ +/* + * 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.systemui.volume; + +import static android.app.slice.Slice.HINT_ERROR; +import static android.app.slice.SliceItem.FORMAT_SLICE; + +import android.content.Context; +import android.net.Uri; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.recyclerview.widget.RecyclerView; +import androidx.slice.Slice; +import androidx.slice.SliceItem; +import androidx.slice.widget.SliceView; + +import com.android.systemui.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * RecyclerView adapter for Slices in Settings Panels. + */ +public class VolumePanelSlicesAdapter extends + RecyclerView.Adapter<VolumePanelSlicesAdapter.SliceRowViewHolder> { + + private final List<LiveData<Slice>> mSliceLiveData; + private final LifecycleOwner mLifecycleOwner; + private SliceView.OnSliceActionListener mOnSliceActionListener; + + public VolumePanelSlicesAdapter(LifecycleOwner lifecycleOwner, + Map<Uri, LiveData<Slice>> sliceLiveData) { + mLifecycleOwner = lifecycleOwner; + mSliceLiveData = new ArrayList<>(sliceLiveData.values()); + } + + @NonNull + @Override + public SliceRowViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + final Context context = viewGroup.getContext(); + final LayoutInflater inflater = LayoutInflater.from(context); + View view = inflater.inflate(R.layout.volume_panel_slice_slider_row, viewGroup, false); + return new SliceRowViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull SliceRowViewHolder sliceRowViewHolder, int position) { + sliceRowViewHolder.onBind(mSliceLiveData.get(position), position); + } + + @Override + public int getItemCount() { + return mSliceLiveData.size(); + } + + @Override + public int getItemViewType(int position) { + return position; + } + + void setOnSliceActionListener(SliceView.OnSliceActionListener listener) { + mOnSliceActionListener = listener; + } + + void updateDataSet(ArrayList<LiveData<Slice>> list) { + mSliceLiveData.clear(); + mSliceLiveData.addAll(list); + notifyDataSetChanged(); + } + + /** + * ViewHolder for binding Slices to SliceViews. + */ + public class SliceRowViewHolder extends RecyclerView.ViewHolder { + + private final SliceView mSliceView; + + public SliceRowViewHolder(View view) { + super(view); + mSliceView = view.findViewById(R.id.slice_view); + mSliceView.setMode(SliceView.MODE_LARGE); + mSliceView.setShowTitleItems(true); + mSliceView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + mSliceView.setOnSliceActionListener(mOnSliceActionListener); + } + + /** + * Called when the view is displayed. + */ + public void onBind(LiveData<Slice> sliceLiveData, int position) { + sliceLiveData.observe(mLifecycleOwner, mSliceView); + + // Do not show the divider above media devices switcher slice per request + final Slice slice = sliceLiveData.getValue(); + + // Hides slice which reports with error hint or not contain any slice sub-item. + if (slice == null || !isValidSlice(slice)) { + mSliceView.setVisibility(View.GONE); + } else { + mSliceView.setVisibility(View.VISIBLE); + } + } + + private boolean isValidSlice(Slice slice) { + if (slice.getHints().contains(HINT_ERROR)) { + return false; + } + for (SliceItem item : slice.getItems()) { + if (item.getFormat().equals(FORMAT_SLICE)) { + return true; + } + } + return false; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index f3855bddfe48..c5792b923e48 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -30,6 +30,7 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.volume.VolumeComponent; import com.android.systemui.volume.VolumeDialogComponent; import com.android.systemui.volume.VolumeDialogImpl; +import com.android.systemui.volume.VolumePanelFactory; import dagger.Binds; import dagger.Module; @@ -52,6 +53,7 @@ public interface VolumeModule { DeviceProvisionedController deviceProvisionedController, ConfigurationController configurationController, MediaOutputDialogFactory mediaOutputDialogFactory, + VolumePanelFactory volumePanelFactory, ActivityStarter activityStarter, InteractionJankMonitor interactionJankMonitor) { VolumeDialogImpl impl = new VolumeDialogImpl( @@ -61,6 +63,7 @@ public interface VolumeModule { deviceProvisionedController, configurationController, mediaOutputDialogFactory, + volumePanelFactory, activityStarter, interactionJankMonitor); impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index aecec9d100cc..d68e8bd36c40 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -387,6 +387,33 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { } @Test + public void onResume_sideFpsHintShouldBeShown_sideFpsHintShown() { + setupGetSecurityView(); + setupConditionsToEnableSideFpsHint(); + mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE); + reset(mSidefpsController); + + mKeyguardSecurityContainerController.onResume(0); + + verify(mSidefpsController).show(); + verify(mSidefpsController, never()).hide(); + } + + @Test + public void onResume_sideFpsHintShouldNotBeShown_sideFpsHintHidden() { + setupGetSecurityView(); + setupConditionsToEnableSideFpsHint(); + setSideFpsHintEnabledFromResources(false); + mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE); + reset(mSidefpsController); + + mKeyguardSecurityContainerController.onResume(0); + + verify(mSidefpsController).hide(); + verify(mSidefpsController, never()).show(); + } + + @Test public void showNextSecurityScreenOrFinish_setsSecurityScreenToPinAfterSimPinUnlock() { // GIVEN the current security method is SimPin when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt new file mode 100644 index 000000000000..6b1ef389a98e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt @@ -0,0 +1,179 @@ +package com.android.systemui + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.content.res.Resources +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flag +import com.android.systemui.flags.FlagListenable +import com.android.systemui.flags.Flags +import com.android.systemui.flags.UnreleasedFlag +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.TestCoroutineDispatcher +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ChooserSelectorTest : SysuiTestCase() { + + private val flagListener = kotlinArgumentCaptor<FlagListenable.Listener>() + + private val testDispatcher = TestCoroutineDispatcher() + private val testScope = CoroutineScope(testDispatcher) + + private lateinit var chooserSelector: ChooserSelector + + @Mock private lateinit var mockContext: Context + @Mock private lateinit var mockPackageManager: PackageManager + @Mock private lateinit var mockResources: Resources + @Mock private lateinit var mockFeatureFlags: FeatureFlags + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + `when`(mockContext.packageManager).thenReturn(mockPackageManager) + `when`(mockContext.resources).thenReturn(mockResources) + `when`(mockResources.getString(anyInt())).thenReturn( + ComponentName("TestPackage", "TestClass").flattenToString()) + + chooserSelector = ChooserSelector(mockContext, mockFeatureFlags, testScope, testDispatcher) + } + + @After + fun tearDown() { + testDispatcher.cleanupTestCoroutines() + } + + @Test + fun initialize_registersFlagListenerUntilScopeCancelled() { + // Arrange + + // Act + chooserSelector.start() + + // Assert + verify(mockFeatureFlags).addListener( + eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture()) + verify(mockFeatureFlags, never()).removeListener(any()) + + // Act + testScope.cancel() + + // Assert + verify(mockFeatureFlags).removeListener(eq(flagListener.value)) + } + + @Test + fun initialize_enablesUnbundledChooser_whenFlagEnabled() { + // Arrange + `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true) + + // Act + chooserSelector.start() + + // Assert + verify(mockPackageManager).setComponentEnabledSetting( + eq(ComponentName("TestPackage", "TestClass")), + eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), + anyInt()) + } + + @Test + fun initialize_disablesUnbundledChooser_whenFlagDisabled() { + // Arrange + `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false) + + // Act + chooserSelector.start() + + // Assert + verify(mockPackageManager).setComponentEnabledSetting( + eq(ComponentName("TestPackage", "TestClass")), + eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), + anyInt()) + } + + @Test + fun enablesUnbundledChooser_whenFlagBecomesEnabled() { + // Arrange + `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false) + chooserSelector.start() + verify(mockFeatureFlags).addListener( + eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture()) + verify(mockPackageManager, never()).setComponentEnabledSetting( + any(), eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), anyInt()) + + // Act + `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true) + flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id)) + + // Assert + verify(mockPackageManager).setComponentEnabledSetting( + eq(ComponentName("TestPackage", "TestClass")), + eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), + anyInt()) + } + + @Test + fun disablesUnbundledChooser_whenFlagBecomesDisabled() { + // Arrange + `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true) + chooserSelector.start() + verify(mockFeatureFlags).addListener( + eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture()) + verify(mockPackageManager, never()).setComponentEnabledSetting( + any(), eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), anyInt()) + + // Act + `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false) + flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id)) + + // Assert + verify(mockPackageManager).setComponentEnabledSetting( + eq(ComponentName("TestPackage", "TestClass")), + eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), + anyInt()) + } + + @Test + fun doesNothing_whenAnotherFlagChanges() { + // Arrange + `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false) + chooserSelector.start() + verify(mockFeatureFlags).addListener( + eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture()) + clearInvocations(mockPackageManager) + + // Act + `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false) + flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id + 1)) + + // Assert + verifyZeroInteractions(mockPackageManager) + } + + private class TestFlagEvent(override val flagId: Int) : FlagListenable.FlagEvent { + override fun requestNoRestart() {} + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt index ff579a1cc4aa..318f2bc1c227 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt @@ -41,7 +41,7 @@ class FakeFeatureFlagsTest : SysuiTestCase() { * specified. If not, an exception is thrown. */ @Test - fun throwsIfUnspecifiedFlagIsAccessed() { + fun accessingUnspecifiedFlags_throwsException() { val flags: FeatureFlags = FakeFeatureFlags() try { assertThat(flags.isEnabled(Flags.TEAMFOOD)).isFalse() @@ -88,7 +88,7 @@ class FakeFeatureFlagsTest : SysuiTestCase() { } @Test - fun specifiedFlagsReturnCorrectValues() { + fun specifiedFlags_returnCorrectValues() { val flags = FakeFeatureFlags() flags.set(unreleasedFlag, false) flags.set(releasedFlag, false) @@ -114,4 +114,125 @@ class FakeFeatureFlagsTest : SysuiTestCase() { assertThat(flags.isEnabled(sysPropBooleanFlag)).isTrue() assertThat(flags.getString(resourceStringFlag)).isEqualTo("Android") } + + @Test + fun listenerForBooleanFlag_calledOnlyWhenFlagChanged() { + val flags = FakeFeatureFlags() + val listener = VerifyingListener() + flags.addListener(unreleasedFlag, listener) + + flags.set(unreleasedFlag, true) + flags.set(unreleasedFlag, true) + flags.set(unreleasedFlag, false) + flags.set(unreleasedFlag, false) + + listener.verifyInOrder(unreleasedFlag.id, unreleasedFlag.id) + } + + @Test + fun listenerForStringFlag_calledOnlyWhenFlagChanged() { + val flags = FakeFeatureFlags() + val listener = VerifyingListener() + flags.addListener(stringFlag, listener) + + flags.set(stringFlag, "Test") + flags.set(stringFlag, "Test") + + listener.verifyInOrder(stringFlag.id) + } + + @Test + fun listenerForBooleanFlag_notCalledAfterRemoved() { + val flags = FakeFeatureFlags() + val listener = VerifyingListener() + flags.addListener(unreleasedFlag, listener) + flags.set(unreleasedFlag, true) + flags.removeListener(listener) + flags.set(unreleasedFlag, false) + + listener.verifyInOrder(unreleasedFlag.id) + } + + @Test + fun listenerForStringFlag_notCalledAfterRemoved() { + val flags = FakeFeatureFlags() + val listener = VerifyingListener() + + flags.addListener(stringFlag, listener) + flags.set(stringFlag, "Test") + flags.removeListener(listener) + flags.set(stringFlag, "Other") + + listener.verifyInOrder(stringFlag.id) + } + + @Test + fun listenerForMultipleFlags_calledWhenFlagsChange() { + val flags = FakeFeatureFlags() + val listener = VerifyingListener() + flags.addListener(unreleasedFlag, listener) + flags.addListener(releasedFlag, listener) + + flags.set(releasedFlag, true) + flags.set(unreleasedFlag, true) + + listener.verifyInOrder(releasedFlag.id, unreleasedFlag.id) + } + + @Test + fun listenerForMultipleFlags_notCalledAfterRemoved() { + val flags = FakeFeatureFlags() + val listener = VerifyingListener() + + flags.addListener(unreleasedFlag, listener) + flags.addListener(releasedFlag, listener) + flags.set(releasedFlag, true) + flags.set(unreleasedFlag, true) + flags.removeListener(listener) + flags.set(releasedFlag, false) + flags.set(unreleasedFlag, false) + + listener.verifyInOrder(releasedFlag.id, unreleasedFlag.id) + } + + @Test + fun multipleListenersForSingleFlag_allAreCalledWhenChanged() { + val flags = FakeFeatureFlags() + val listener1 = VerifyingListener() + val listener2 = VerifyingListener() + flags.addListener(releasedFlag, listener1) + flags.addListener(releasedFlag, listener2) + + flags.set(releasedFlag, true) + + listener1.verifyInOrder(releasedFlag.id) + listener2.verifyInOrder(releasedFlag.id) + } + + @Test + fun multipleListenersForSingleFlag_removedListenerNotCalledAfterRemoval() { + val flags = FakeFeatureFlags() + val listener1 = VerifyingListener() + val listener2 = VerifyingListener() + flags.addListener(releasedFlag, listener1) + flags.addListener(releasedFlag, listener2) + + flags.set(releasedFlag, true) + flags.removeListener(listener2) + flags.set(releasedFlag, false) + + listener1.verifyInOrder(releasedFlag.id, releasedFlag.id) + listener2.verifyInOrder(releasedFlag.id) + } + + class VerifyingListener : FlagListenable.Listener { + var flagEventIds = mutableListOf<Int>() + override fun onFlagChanged(event: FlagListenable.FlagEvent) { + flagEventIds.add(event.flagId) + } + + fun verifyInOrder(vararg eventIds: Int) { + assertThat(flagEventIds).containsExactlyElementsIn(eventIds.asList()) + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt index 178502269e73..bef46953395b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt @@ -1051,6 +1051,17 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test + fun bindDeviceWithNullName() { + val fallbackString = context.getResources().getString(R.string.media_seamless_other_device) + player.attachPlayer(viewHolder) + val state = mediaData.copy(device = device.copy(name = null)) + player.bindPlayer(state, PACKAGE) + assertThat(seamless.isEnabled()).isTrue() + assertThat(seamlessText.getText()).isEqualTo(fallbackString) + assertThat(seamless.contentDescription).isEqualTo(fallbackString) + } + + @Test fun bindDeviceResumptionPlayer() { player.attachPlayer(viewHolder) val state = mediaData.copy(resumption = true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt index d1ed8e983cdd..f9c7d2d5cb41 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -31,7 +31,6 @@ import com.android.systemui.statusbar.SbnBuilder import com.android.systemui.tuner.TunerService import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock @@ -108,6 +107,7 @@ class MediaDataManagerTest : SysuiTestCase() { private val clock = FakeSystemClock() @Mock private lateinit var tunerService: TunerService @Captor lateinit var tunableCaptor: ArgumentCaptor<TunerService.Tunable> + @Captor lateinit var callbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit> private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20) @@ -974,7 +974,6 @@ class MediaDataManagerTest : SysuiTestCase() { fun testPlaybackStateChange_keyExists_callsListener() { // Notification has been added addNotificationAndLoad() - val callbackCaptor = argumentCaptor<(String, PlaybackState) -> Unit>() verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) // Callback gets an updated state @@ -992,7 +991,6 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testPlaybackStateChange_keyDoesNotExist_doesNothing() { val state = PlaybackState.Builder().build() - val callbackCaptor = argumentCaptor<(String, PlaybackState) -> Unit>() verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) // No media added with this key @@ -1013,7 +1011,6 @@ class MediaDataManagerTest : SysuiTestCase() { // And then get a state update val state = PlaybackState.Builder().build() - val callbackCaptor = argumentCaptor<(String, PlaybackState) -> Unit>() verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) // Then no changes are made @@ -1022,6 +1019,83 @@ class MediaDataManagerTest : SysuiTestCase() { anyBoolean()) } + @Test + fun testPlaybackState_PauseWhenFlagTrue_keyExists_callsListener() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val state = PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 1f) + .build() + whenever(controller.playbackState).thenReturn(state) + + addNotificationAndLoad() + verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) + callbackCaptor.value.invoke(KEY, state) + + verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), + capture(mediaDataCaptor), eq(true), eq(0), eq(false)) + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + assertThat(mediaDataCaptor.value.semanticActions).isNotNull() + } + + @Test + fun testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListener() { + val desc = MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + val state = PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 1f) + .setActions(PlaybackState.ACTION_PLAY_PAUSE) + .build() + + // Add resumption controls in order to have semantic actions. + // To make sure that they are not null after changing state. + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + backgroundExecutor.runAllReady() + foregroundExecutor.runAllReady() + + verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) + callbackCaptor.value.invoke(PACKAGE_NAME, state) + + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(PACKAGE_NAME), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + assertThat(mediaDataCaptor.value.semanticActions).isNotNull() + } + + @Test + fun testPlaybackStateNull_Pause_keyExists_callsListener() { + whenever(controller.playbackState).thenReturn(null) + val state = PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 1f) + .setActions(PlaybackState.ACTION_PLAY_PAUSE) + .build() + + addNotificationAndLoad() + verify(mediaTimeoutListener).stateCallback = capture(callbackCaptor) + callbackCaptor.value.invoke(KEY, state) + + verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), + capture(mediaDataCaptor), eq(true), eq(0), eq(false)) + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + assertThat(mediaDataCaptor.value.semanticActions).isNull() + } + /** * Helper function to add a media notification and capture the resulting MediaData */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt index ee104262dc29..121c8946d164 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt @@ -59,8 +59,8 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions -import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever +import org.mockito.junit.MockitoJUnit private const val KEY = "TEST_KEY" private const val KEY_OLD = "TEST_KEY_OLD" @@ -402,9 +402,10 @@ public class MediaDeviceManagerTest : SysuiTestCase() { manager.onMediaDataLoaded(KEY, null, mediaData) fakeBgExecutor.runAllReady() fakeFgExecutor.runAllReady() - // THEN the device is disabled + // THEN the device is disabled and name is set to null val data = captureDeviceData(KEY) assertThat(data.enabled).isFalse() + assertThat(data.name).isNull() } @Test @@ -421,9 +422,10 @@ public class MediaDeviceManagerTest : SysuiTestCase() { deviceCallback.onSelectedDeviceStateChanged(device, 1) fakeBgExecutor.runAllReady() fakeFgExecutor.runAllReady() - // THEN the device is disabled + // THEN the device is disabled and name is set to null val data = captureDeviceData(KEY) assertThat(data.enabled).isFalse() + assertThat(data.name).isNull() } @Test @@ -440,9 +442,24 @@ public class MediaDeviceManagerTest : SysuiTestCase() { deviceCallback.onDeviceListUpdate(mutableListOf(device)) fakeBgExecutor.runAllReady() fakeFgExecutor.runAllReady() - // THEN the device is disabled + // THEN the device is disabled and name is set to null val data = captureDeviceData(KEY) assertThat(data.enabled).isFalse() + assertThat(data.name).isNull() + } + + @Test + fun mr2ReturnsRouteWithNullName_useLocalDeviceName() { + // GIVEN that MR2Manager returns a routing session that does not have a name + whenever(route.name).thenReturn(null) + // WHEN a notification is added + manager.onMediaDataLoaded(KEY, null, mediaData) + fakeBgExecutor.runAllReady() + fakeFgExecutor.runAllReady() + // THEN the device is enabled and uses the current connected device name + val data = captureDeviceData(KEY) + assertThat(data.name).isEqualTo(DEVICE_NAME) + assertThat(data.enabled).isTrue() } @Test @@ -647,12 +664,14 @@ public class MediaDeviceManagerTest : SysuiTestCase() { override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {} override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {} - override fun onBroadcastMetadataChanged(broadcastId: Int, - metadata: BluetoothLeBroadcastMetadata) {} + override fun onBroadcastMetadataChanged( + broadcastId: Int, + metadata: BluetoothLeBroadcastMetadata + ) {} } bluetoothLeBroadcast.registerCallback(fakeFgExecutor, callback) - return callback; + return callback } fun setupLeAudioConfiguration(isLeAudio: Boolean) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java index 260bb8760f1c..22ecb4b93743 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -78,7 +78,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices); when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false); - when(mMediaOutputController.isTransferring()).thenReturn(false); + when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false); when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat); when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat); when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1); @@ -208,7 +208,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() { - when(mMediaOutputController.isTransferring()).thenReturn(true); + when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true); when(mMediaDevice1.getState()).thenReturn( LocalMediaManager.MediaDeviceState.STATE_CONNECTING); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); @@ -224,7 +224,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @Test public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() { - when(mMediaOutputController.isTransferring()).thenReturn(true); + when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true); when(mMediaDevice2.getState()).thenReturn( LocalMediaManager.MediaDeviceState.STATE_CONNECTING); mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt index 1061e3c6b0d5..fa47a746f8ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt @@ -35,7 +35,9 @@ import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.FalsingCollector import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.FakeExecutor @@ -48,11 +50,12 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @@ -78,6 +81,10 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { private lateinit var viewUtil: ViewUtil @Mock private lateinit var commandQueue: CommandQueue + @Mock + private lateinit var falsingManager: FalsingManager + @Mock + private lateinit var falsingCollector: FalsingCollector private lateinit var commandQueueCallback: CommandQueue.Callbacks private lateinit var fakeAppIconDrawable: Drawable private lateinit var fakeClock: FakeSystemClock @@ -115,7 +122,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { accessibilityManager, configurationController, powerManager, - senderUiEventLogger + senderUiEventLogger, + falsingManager, + falsingCollector ) val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) @@ -421,6 +430,38 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { } @Test + fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(true) + var undoCallbackCalled = false + val undoCallback = object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + } + + controllerSender.displayChip(transferToReceiverSucceeded(undoCallback)) + getChipView().getUndoButton().performClick() + + assertThat(undoCallbackCalled).isFalse() + } + + @Test + fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() { + whenever(falsingManager.isFalseTap(anyInt())).thenReturn(false) + var undoCallbackCalled = false + val undoCallback = object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + } + + controllerSender.displayChip(transferToReceiverSucceeded(undoCallback)) + getChipView().getUndoButton().performClick() + + assertThat(undoCallbackCalled).isTrue() + } + + @Test fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { val undoCallback = object : IUndoMediaTransferCallback.Stub() { override fun onUndoTriggered() {} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt index 48fbd354b98d..073c23cec569 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt @@ -23,6 +23,7 @@ import android.graphics.Insets import android.graphics.Rect import android.hardware.HardwareBuffer import android.os.Bundle +import android.os.UserHandle import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN @@ -97,7 +98,7 @@ class RequestProcessorTest { policy.setManagedProfile(USER_ID, false) policy.setDisplayContentInfo( policy.getDefaultDisplayId(), - DisplayContentInfo(component, bounds, USER_ID, TASK_ID)) + DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)) val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD) val processor = RequestProcessor(imageCapture, policy, flags, scope) @@ -120,7 +121,7 @@ class RequestProcessorTest { // Indicate that the primary content belongs to a manged profile policy.setManagedProfile(USER_ID, true) policy.setDisplayContentInfo(policy.getDefaultDisplayId(), - DisplayContentInfo(component, bounds, USER_ID, TASK_ID)) + DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)) val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD) val processor = RequestProcessor(imageCapture, policy, flags, scope) @@ -160,7 +161,7 @@ class RequestProcessorTest { policy.setManagedProfile(USER_ID, false) policy.setDisplayContentInfo(policy.getDefaultDisplayId(), - DisplayContentInfo(component, bounds, USER_ID, TASK_ID)) + DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)) val processedRequest = processor.process(request) @@ -183,7 +184,7 @@ class RequestProcessorTest { // Indicate that the primary content belongs to a manged profile policy.setManagedProfile(USER_ID, true) policy.setDisplayContentInfo(policy.getDefaultDisplayId(), - DisplayContentInfo(component, bounds, USER_ID, TASK_ID)) + DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID)) val processedRequest = processor.process(request) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt new file mode 100644 index 000000000000..17396b13036c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt @@ -0,0 +1,227 @@ +/* + * 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.systemui.screenshot + +import android.app.ActivityTaskManager.RootTaskInfo +import android.app.IActivityTaskManager +import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED +import android.content.ComponentName +import android.content.Context +import android.graphics.Rect +import android.os.UserHandle +import android.os.UserManager +import android.testing.AndroidTestingRunner +import com.android.systemui.SysuiTestCase +import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith + +// The following values are chosen to be distinct from commonly seen real values +private const val DISPLAY_ID = 100 +private const val PRIMARY_USER = 2000 +private const val MANAGED_PROFILE_USER = 3000 + +@RunWith(AndroidTestingRunner::class) +class ScreenshotPolicyImplTest : SysuiTestCase() { + + @Test + fun testToDisplayContentInfo() { + assertThat(fullScreenWorkProfileTask.toDisplayContentInfo()) + .isEqualTo( + DisplayContentInfo( + ComponentName( + "com.google.android.apps.nbu.files", + "com.google.android.apps.nbu.files.home.HomeActivity" + ), + Rect(0, 0, 1080, 2400), + UserHandle.of(MANAGED_PROFILE_USER), + 65)) + } + + @Test + fun findPrimaryContent_ignoresPipTask() = runBlocking { + val policy = fakeTasksPolicyImpl( + mContext, + shadeExpanded = false, + tasks = listOf( + pipTask, + fullScreenWorkProfileTask, + launcherTask, + emptyTask) + ) + + val info = policy.findPrimaryContent(DISPLAY_ID) + assertThat(info).isEqualTo(fullScreenWorkProfileTask.toDisplayContentInfo()) + } + + @Test + fun findPrimaryContent_shadeExpanded_ignoresTopTask() = runBlocking { + val policy = fakeTasksPolicyImpl( + mContext, + shadeExpanded = true, + tasks = listOf( + fullScreenWorkProfileTask, + launcherTask, + emptyTask) + ) + + val info = policy.findPrimaryContent(DISPLAY_ID) + assertThat(info).isEqualTo(policy.systemUiContent) + } + + @Test + fun findPrimaryContent_emptyTaskList() = runBlocking { + val policy = fakeTasksPolicyImpl( + mContext, + shadeExpanded = false, + tasks = listOf() + ) + + val info = policy.findPrimaryContent(DISPLAY_ID) + assertThat(info).isEqualTo(policy.systemUiContent) + } + + @Test + fun findPrimaryContent_workProfileNotOnTop() = runBlocking { + val policy = fakeTasksPolicyImpl( + mContext, + shadeExpanded = false, + tasks = listOf( + launcherTask, + fullScreenWorkProfileTask, + emptyTask) + ) + + val info = policy.findPrimaryContent(DISPLAY_ID) + assertThat(info).isEqualTo(launcherTask.toDisplayContentInfo()) + } + + private fun fakeTasksPolicyImpl( + context: Context, + shadeExpanded: Boolean, + tasks: List<RootTaskInfo> + ): ScreenshotPolicyImpl { + val userManager = mock<UserManager>() + val atmService = mock<IActivityTaskManager>() + val dispatcher = Dispatchers.Unconfined + + return object : ScreenshotPolicyImpl(context, userManager, atmService, dispatcher) { + override suspend fun isManagedProfile(userId: Int) = (userId == MANAGED_PROFILE_USER) + override suspend fun getAllRootTaskInfosOnDisplay(displayId: Int) = tasks + override suspend fun isNotificationShadeExpanded() = shadeExpanded + } + } + + private val pipTask = RootTaskInfo().apply { + configuration.windowConfiguration.apply { + windowingMode = WINDOWING_MODE_PINNED + bounds = Rect(628, 1885, 1038, 2295) + activityType = ACTIVITY_TYPE_STANDARD + } + displayId = DISPLAY_ID + userId = PRIMARY_USER + taskId = 66 + visible = true + isVisible = true + isRunning = true + numActivities = 1 + topActivity = ComponentName( + "com.google.android.youtube", + "com.google.android.apps.youtube.app.watchwhile.WatchWhileActivity" + ) + childTaskIds = intArrayOf(66) + childTaskNames = arrayOf("com.google.android.youtube/" + + "com.google.android.youtube.app.honeycomb.Shell\$HomeActivity") + childTaskUserIds = intArrayOf(0) + childTaskBounds = arrayOf(Rect(628, 1885, 1038, 2295)) + } + + private val fullScreenWorkProfileTask = RootTaskInfo().apply { + configuration.windowConfiguration.apply { + windowingMode = WINDOWING_MODE_FULLSCREEN + bounds = Rect(0, 0, 1080, 2400) + activityType = ACTIVITY_TYPE_STANDARD + } + displayId = DISPLAY_ID + userId = MANAGED_PROFILE_USER + taskId = 65 + visible = true + isVisible = true + isRunning = true + numActivities = 1 + topActivity = ComponentName( + "com.google.android.apps.nbu.files", + "com.google.android.apps.nbu.files.home.HomeActivity" + ) + childTaskIds = intArrayOf(65) + childTaskNames = arrayOf("com.google.android.apps.nbu.files/" + + "com.google.android.apps.nbu.files.home.HomeActivity") + childTaskUserIds = intArrayOf(MANAGED_PROFILE_USER) + childTaskBounds = arrayOf(Rect(0, 0, 1080, 2400)) + } + + private val launcherTask = RootTaskInfo().apply { + configuration.windowConfiguration.apply { + windowingMode = WINDOWING_MODE_FULLSCREEN + bounds = Rect(0, 0, 1080, 2400) + activityType = ACTIVITY_TYPE_HOME + } + displayId = DISPLAY_ID + taskId = 1 + userId = PRIMARY_USER + visible = true + isVisible = true + isRunning = true + numActivities = 1 + topActivity = ComponentName( + "com.google.android.apps.nexuslauncher", + "com.google.android.apps.nexuslauncher.NexusLauncherActivity", + ) + childTaskIds = intArrayOf(1) + childTaskNames = arrayOf("com.google.android.apps.nexuslauncher/" + + "com.google.android.apps.nexuslauncher.NexusLauncherActivity") + childTaskUserIds = intArrayOf(0) + childTaskBounds = arrayOf(Rect(0, 0, 1080, 2400)) + } + + private val emptyTask = RootTaskInfo().apply { + configuration.windowConfiguration.apply { + windowingMode = WINDOWING_MODE_FULLSCREEN + bounds = Rect(0, 0, 1080, 2400) + activityType = ACTIVITY_TYPE_UNDEFINED + } + displayId = DISPLAY_ID + taskId = 2 + userId = PRIMARY_USER + visible = false + isVisible = false + isRunning = false + numActivities = 0 + childTaskIds = intArrayOf(3, 4) + childTaskNames = arrayOf("", "") + childTaskUserIds = intArrayOf(0, 0) + childTaskBounds = arrayOf(Rect(0, 0, 1080, 2400), Rect(0, 2400, 1080, 4800)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 98389c2c7a6f..e2ce939cb66c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -458,7 +458,6 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { NotificationWakeUpCoordinator coordinator = new NotificationWakeUpCoordinator( - mDumpManager, mock(HeadsUpManagerPhone.class), new StatusBarStateControllerImpl(new UiEventLoggerFake(), mDumpManager, mInteractionJankMonitor), diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 312db2d7066a..2e74bf5474f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -85,6 +85,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Mock MediaOutputDialogFactory mMediaOutputDialogFactory; @Mock + VolumePanelFactory mVolumePanelFactory; + @Mock ActivityStarter mActivityStarter; @Mock InteractionJankMonitor mInteractionJankMonitor; @@ -102,6 +104,7 @@ public class VolumeDialogImplTest extends SysuiTestCase { mDeviceProvisionedController, mConfigurationController, mMediaOutputDialogFactory, + mVolumePanelFactory, mActivityStarter, mInteractionJankMonitor); mDialog.init(0, null); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt index b53ad0a3726f..c56fdb17b5f1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt @@ -16,14 +16,12 @@ package com.android.systemui.flags -import android.util.SparseArray -import android.util.SparseBooleanArray -import androidx.core.util.containsKey - class FakeFeatureFlags : FeatureFlags { - private val booleanFlags = SparseBooleanArray() - private val stringFlags = SparseArray<String>() + private val booleanFlags = mutableMapOf<Int, Boolean>() + private val stringFlags = mutableMapOf<Int, String>() private val knownFlagNames = mutableMapOf<Int, String>() + private val flagListeners = mutableMapOf<Int, MutableSet<FlagListenable.Listener>>() + private val listenerFlagIds = mutableMapOf<FlagListenable.Listener, MutableSet<Int>>() init { Flags.getFlagFields().forEach { field -> @@ -33,27 +31,52 @@ class FakeFeatureFlags : FeatureFlags { } fun set(flag: BooleanFlag, value: Boolean) { - booleanFlags.put(flag.id, value) + if (booleanFlags.put(flag.id, value)?.let { value != it } != false) { + notifyFlagChanged(flag) + } } fun set(flag: DeviceConfigBooleanFlag, value: Boolean) { - booleanFlags.put(flag.id, value) + if (booleanFlags.put(flag.id, value)?.let { value != it } != false) { + notifyFlagChanged(flag) + } } fun set(flag: ResourceBooleanFlag, value: Boolean) { - booleanFlags.put(flag.id, value) + if (booleanFlags.put(flag.id, value)?.let { value != it } != false) { + notifyFlagChanged(flag) + } } fun set(flag: SysPropBooleanFlag, value: Boolean) { - booleanFlags.put(flag.id, value) + if (booleanFlags.put(flag.id, value)?.let { value != it } != false) { + notifyFlagChanged(flag) + } } fun set(flag: StringFlag, value: String) { - stringFlags.put(flag.id, value) + if (stringFlags.put(flag.id, value)?.let { value != it } == null) { + notifyFlagChanged(flag) + } } fun set(flag: ResourceStringFlag, value: String) { - stringFlags.put(flag.id, value) + if (stringFlags.put(flag.id, value)?.let { value != it } == null) { + notifyFlagChanged(flag) + } + } + + private fun notifyFlagChanged(flag: Flag<*>) { + flagListeners[flag.id]?.let { listeners -> + listeners.forEach { listener -> + listener.onFlagChanged( + object : FlagListenable.FlagEvent { + override val flagId = flag.id + override fun requestNoRestart() {} + } + ) + } + } } override fun isEnabled(flag: UnreleasedFlag): Boolean = requireBooleanValue(flag.id) @@ -70,25 +93,30 @@ class FakeFeatureFlags : FeatureFlags { override fun getString(flag: ResourceStringFlag): String = requireStringValue(flag.id) - override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {} + override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) { + flagListeners.getOrPut(flag.id) { mutableSetOf() }.add(listener) + listenerFlagIds.getOrPut(listener) { mutableSetOf() }.add(flag.id) + } - override fun removeListener(listener: FlagListenable.Listener) {} + override fun removeListener(listener: FlagListenable.Listener) { + listenerFlagIds.remove(listener)?.let { + flagIds -> flagIds.forEach { + id -> flagListeners[id]?.remove(listener) + } + } + } private fun flagName(flagId: Int): String { return knownFlagNames[flagId] ?: "UNKNOWN(id=$flagId)" } private fun requireBooleanValue(flagId: Int): Boolean { - if (!booleanFlags.containsKey(flagId)) { - throw IllegalStateException("Flag ${flagName(flagId)} was accessed but not specified.") - } return booleanFlags[flagId] + ?: error("Flag ${flagName(flagId)} was accessed but not specified.") } private fun requireStringValue(flagId: Int): String { - if (!stringFlags.containsKey(flagId)) { - throw IllegalStateException("Flag ${flagName(flagId)} was accessed but not specified.") - } return stringFlags[flagId] + ?: error("Flag ${flagName(flagId)} was accessed but not specified.") } } diff --git a/services/core/java/com/android/server/ambientcontext/AmbientContextShellCommand.java b/services/core/java/com/android/server/ambientcontext/AmbientContextShellCommand.java index e2b22dc1bd3d..ec6c2f00b3b1 100644 --- a/services/core/java/com/android/server/ambientcontext/AmbientContextShellCommand.java +++ b/services/core/java/com/android/server/ambientcontext/AmbientContextShellCommand.java @@ -21,12 +21,12 @@ import static java.lang.System.out; import android.annotation.NonNull; import android.app.ambientcontext.AmbientContextEvent; import android.app.ambientcontext.AmbientContextEventRequest; +import android.app.ambientcontext.AmbientContextManager; import android.content.ComponentName; import android.os.Binder; import android.os.RemoteCallback; import android.os.ShellCommand; import android.service.ambientcontext.AmbientContextDetectionResult; -import android.service.ambientcontext.AmbientContextDetectionServiceStatus; import java.io.PrintWriter; @@ -51,13 +51,13 @@ final class AmbientContextShellCommand extends ShellCommand { /** Callbacks for AmbientContextEventService results used internally for testing. */ static class TestableCallbackInternal { private AmbientContextDetectionResult mLastResult; - private AmbientContextDetectionServiceStatus mLastStatus; + private int mLastStatus; public AmbientContextDetectionResult getLastResult() { return mLastResult; } - public AmbientContextDetectionServiceStatus getLastStatus() { + public int getLastStatus() { return mLastStatus; } @@ -80,13 +80,10 @@ final class AmbientContextShellCommand extends ShellCommand { @NonNull private RemoteCallback createRemoteStatusCallback() { return new RemoteCallback(result -> { - AmbientContextDetectionServiceStatus status = - (AmbientContextDetectionServiceStatus) result.get( - AmbientContextDetectionServiceStatus.STATUS_RESPONSE_BUNDLE_KEY); + int status = result.getInt(AmbientContextManager.STATUS_RESPONSE_BUNDLE_KEY); final long token = Binder.clearCallingIdentity(); try { mLastStatus = status; - out.println("Status available: " + status); } finally { Binder.restoreCallingIdentity(token); } @@ -110,8 +107,6 @@ final class AmbientContextShellCommand extends ShellCommand { return runStopDetection(); case "get-last-status-code": return getLastStatusCode(); - case "get-last-package-name": - return getLastPackageName(); case "query-service-status": return runQueryServiceStatus(); case "get-bound-package": @@ -126,7 +121,8 @@ final class AmbientContextShellCommand extends ShellCommand { private int runStartDetection() { final int userId = Integer.parseInt(getNextArgRequired()); final String packageName = getNextArgRequired(); - mService.startDetection(userId, REQUEST, packageName, + mService.startDetection( + userId, REQUEST, packageName, sTestableCallbackInternal.createRemoteDetectionResultCallback(), sTestableCallbackInternal.createRemoteStatusCallback()); return 0; @@ -151,18 +147,9 @@ final class AmbientContextShellCommand extends ShellCommand { } private int getLastStatusCode() { - AmbientContextDetectionServiceStatus lastResponse = - sTestableCallbackInternal.getLastStatus(); - if (lastResponse == null) { - return -1; - } - return lastResponse.getStatusCode(); - } - - private int getLastPackageName() { - AmbientContextDetectionServiceStatus lastResponse = - sTestableCallbackInternal.getLastStatus(); - out.println(lastResponse == null ? "" : lastResponse.getPackageName()); + final PrintWriter resultPrinter = getOutPrintWriter(); + int lastStatus = sTestableCallbackInternal.getLastStatus(); + resultPrinter.println(lastStatus); return 0; } @@ -174,22 +161,21 @@ final class AmbientContextShellCommand extends ShellCommand { pw.println(" Print this help text."); pw.println(); pw.println(" start-detection USER_ID PACKAGE_NAME: Starts AmbientContextEvent detection."); - pw.println(" stop-detection USER_ID: Stops AmbientContextEvent detection."); + pw.println(" stop-detection USER_ID PACKAGE_NAME: Stops AmbientContextEvent detection."); pw.println(" get-last-status-code: Prints the latest request status code."); - pw.println(" get-last-package-name: Prints the latest request package name."); - pw.println(" query-event-status USER_ID PACKAGE_NAME: Prints the event status code."); + pw.println(" query-service-status USER_ID PACKAGE_NAME: Prints the service status code."); pw.println(" get-bound-package USER_ID:" + " Print the bound package that implements the service."); - pw.println(" set-temporary-service USER_ID [COMPONENT_NAME DURATION]"); + pw.println(" set-temporary-service USER_ID [PACKAGE_NAME] [COMPONENT_NAME DURATION]"); pw.println(" Temporarily (for DURATION ms) changes the service implementation."); pw.println(" To reset, call with just the USER_ID argument."); } private int getBoundPackageName() { - final PrintWriter out = getOutPrintWriter(); + final PrintWriter resultPrinter = getOutPrintWriter(); final int userId = Integer.parseInt(getNextArgRequired()); final ComponentName componentName = mService.getComponentName(userId); - out.println(componentName == null ? "" : componentName.getPackageName()); + resultPrinter.println(componentName == null ? "" : componentName.getPackageName()); return 0; } diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java index 25d0752844fd..c835d2fe1bbd 100644 --- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java +++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java @@ -116,8 +116,10 @@ public abstract class BrightnessMappingStrategy { luxLevels = getLuxLevels(resources.getIntArray( com.android.internal.R.array.config_autoBrightnessLevelsIdle)); } else { - brightnessLevelsNits = displayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(); - luxLevels = displayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(); + brightnessLevelsNits = getFloatArray(resources.obtainTypedArray( + com.android.internal.R.array.config_autoBrightnessDisplayValuesNits)); + luxLevels = getLuxLevels(resources.getIntArray( + com.android.internal.R.array.config_autoBrightnessLevels)); } // Display independent, mode independent values diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 3b627ef6a786..4f3fd6409cd8 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -20,7 +20,6 @@ import android.annotation.NonNull; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; -import android.content.res.TypedArray; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.DisplayManagerInternal.RefreshRateLimitation; import android.os.Environment; @@ -150,22 +149,12 @@ import javax.xml.datatype.DatatypeConfigurationException; * </quirks> * * <autoBrightness> - * <brighteningLightDebounceMillis> + * <brighteningLightDebounceMillis> * 2000 - * </brighteningLightDebounceMillis> + * </brighteningLightDebounceMillis> * <darkeningLightDebounceMillis> * 1000 * </darkeningLightDebounceMillis> - * <displayBrightnessMapping> - * <displayBrightnessPoint> - * <lux>50</lux> - * <nits>45</nits> - * </displayBrightnessPoint> - * <displayBrightnessPoint> - * <lux>80</lux> - * <nits>75</nits> - * </displayBrightnessPoint> - * </displayBrightnessMapping> * </autoBrightness> * * <screenBrightnessRampFastDecrease>0.01</screenBrightnessRampFastDecrease> @@ -279,39 +268,6 @@ public class DisplayDeviceConfig { // for the corresponding values above private float[] mBrightness; - - /** - * Array of desired screen brightness in nits corresponding to the lux values - * in the mBrightnessLevelsLux array. The display brightness is defined as the - * measured brightness of an all-white image. The brightness values must be non-negative and - * non-decreasing. This must be overridden in platform specific overlays - */ - private float[] mBrightnessLevelsNits; - - /** - * Array of light sensor lux values to define our levels for auto backlight - * brightness support. - * The N entries of this array define N + 1 control points as follows: - * (1-based arrays) - * - * Point 1: (0, value[1]): lux <= 0 - * Point 2: (level[1], value[2]): 0 < lux <= level[1] - * Point 3: (level[2], value[3]): level[2] < lux <= level[3] - * ... - * Point N+1: (level[N], value[N+1]): level[N] < lux - * - * The control points must be strictly increasing. Each control point - * corresponds to an entry in the brightness backlight values arrays. - * For example, if lux == level[1] (first element of the levels array) - * then the brightness will be determined by value[2] (second element - * of the brightness values array). - * - * Spline interpolation is used to determine the auto-brightness - * backlight values for lux levels between these control points. - * - */ - private float[] mBrightnessLevelsLux; - private float mBacklightMinimum = Float.NaN; private float mBacklightMaximum = Float.NaN; private float mBrightnessDefault = Float.NaN; @@ -705,20 +661,6 @@ public class DisplayDeviceConfig { return mAutoBrightnessBrighteningLightDebounce; } - /** - * @return Auto brightness brightening ambient lux levels - */ - public float[] getAutoBrightnessBrighteningLevelsLux() { - return mBrightnessLevelsLux; - } - - /** - * @return Auto brightness brightening nits levels - */ - public float[] getAutoBrightnessBrighteningLevelsNits() { - return mBrightnessLevelsNits; - } - @Override public String toString() { return "DisplayDeviceConfig{" @@ -761,8 +703,6 @@ public class DisplayDeviceConfig { + mAutoBrightnessBrighteningLightDebounce + ", mAutoBrightnessDarkeningLightDebounce= " + mAutoBrightnessDarkeningLightDebounce - + ", mBrightnessLevelsLux= " + Arrays.toString(mBrightnessLevelsLux) - + ", mBrightnessLevelsNits= " + Arrays.toString(mBrightnessLevelsNits) + "}"; } @@ -839,7 +779,6 @@ public class DisplayDeviceConfig { loadBrightnessRampsFromConfigXml(); loadAmbientLightSensorFromConfigXml(); setProxSensorUnspecified(); - loadAutoBrightnessConfigsFromConfigXml(); mLoadedFrom = "<config.xml>"; } @@ -1052,7 +991,6 @@ public class DisplayDeviceConfig { private void loadAutoBrightnessConfigValues(DisplayConfiguration config) { loadAutoBrightnessBrighteningLightDebounce(config.getAutoBrightness()); loadAutoBrightnessDarkeningLightDebounce(config.getAutoBrightness()); - loadAutoBrightnessDisplayBrightnessMapping(config.getAutoBrightness()); } /** @@ -1085,33 +1023,6 @@ public class DisplayDeviceConfig { } } - /** - * Loads the auto-brightness display brightness mappings. Internally, this takes care of - * loading the value from the display config, and if not present, falls back to config.xml. - */ - private void loadAutoBrightnessDisplayBrightnessMapping(AutoBrightness autoBrightnessConfig) { - if (autoBrightnessConfig == null - || autoBrightnessConfig.getDisplayBrightnessMapping() == null) { - mBrightnessLevelsNits = getFloatArray(mContext.getResources() - .obtainTypedArray(com.android.internal.R.array - .config_autoBrightnessDisplayValuesNits)); - mBrightnessLevelsLux = getFloatArray(mContext.getResources() - .obtainTypedArray(com.android.internal.R.array - .config_autoBrightnessLevels)); - } else { - final int size = autoBrightnessConfig.getDisplayBrightnessMapping() - .getDisplayBrightnessPoint().size(); - mBrightnessLevelsNits = new float[size]; - mBrightnessLevelsLux = new float[size]; - for (int i = 0; i < size; i++) { - mBrightnessLevelsNits[i] = autoBrightnessConfig.getDisplayBrightnessMapping() - .getDisplayBrightnessPoint().get(i).getNits().floatValue(); - mBrightnessLevelsLux[i] = autoBrightnessConfig.getDisplayBrightnessMapping() - .getDisplayBrightnessPoint().get(i).getLux().floatValue(); - } - } - } - private void loadBrightnessMapFromConfigXml() { // Use the config.xml mapping final Resources res = mContext.getResources(); @@ -1337,10 +1248,6 @@ public class DisplayDeviceConfig { com.android.internal.R.string.config_displayLightSensorType); } - private void loadAutoBrightnessConfigsFromConfigXml() { - loadAutoBrightnessDisplayBrightnessMapping(null /*AutoBrightnessConfig*/); - } - private void loadAmbientLightSensorFromDdc(DisplayConfiguration config) { final SensorDetails sensorDetails = config.getLightSensor(); if (sensorDetails != null) { @@ -1483,22 +1390,6 @@ public class DisplayDeviceConfig { } } - /** - * Extracts a float array from the specified {@link TypedArray}. - * - * @param array The array to convert. - * @return the given array as a float array. - */ - public static float[] getFloatArray(TypedArray array) { - final int n = array.length(); - float[] vals = new float[n]; - for (int i = 0; i < n; i++) { - vals[i] = array.getFloat(i, PowerManager.BRIGHTNESS_OFF_FLOAT); - } - array.recycle(); - return vals; - } - static class SensorData { public String type; public String name; diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index a174c54eae98..6fcd285fde79 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1582,13 +1582,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (newParent != null && isState(RESUMED)) { newParent.setResumedActivity(this, "onParentChanged"); - if (mStartingWindow != null && mStartingData != null - && mStartingData.mAssociatedTask == null && newParent.isEmbedded()) { - // The starting window should keep covering its task when the activity is - // reparented to a task fragment that may not fill the task bounds. - associateStartingDataWithTask(); - attachStartingSurfaceToAssociatedTask(); - } mImeInsetsFrozenUntilStartInput = false; } @@ -2679,14 +2672,17 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } + /** Called when the starting window is added to this activity. */ void attachStartingWindow(@NonNull WindowState startingWindow) { startingWindow.mStartingData = mStartingData; mStartingWindow = startingWindow; + // The snapshot type may have called associateStartingDataWithTask(). if (mStartingData != null && mStartingData.mAssociatedTask != null) { attachStartingSurfaceToAssociatedTask(); } } + /** Makes starting window always fill the associated task. */ private void attachStartingSurfaceToAssociatedTask() { // Associate the configuration of starting window with the task. overrideConfigurationPropagation(mStartingWindow, mStartingData.mAssociatedTask); @@ -2694,6 +2690,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mStartingData.mAssociatedTask.mSurfaceControl); } + /** Called when the starting window is not added yet but its data is known to fill the task. */ private void associateStartingDataWithTask() { mStartingData.mAssociatedTask = task; task.forAllActivities(r -> { @@ -2703,6 +2700,16 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A }); } + /** Associates and attaches an added starting window to the current task. */ + void associateStartingWindowWithTaskIfNeeded() { + if (mStartingWindow == null || mStartingData == null + || mStartingData.mAssociatedTask != null) { + return; + } + associateStartingDataWithTask(); + attachStartingSurfaceToAssociatedTask(); + } + void removeStartingWindow() { boolean prevEligibleForLetterboxEducation = isEligibleForLetterboxEducation(); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 0376974900e3..b64409c21280 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -3176,8 +3176,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (DEBUG_DISPLAY) Slog.v(TAG_WM, "Removing display=" + this); mPointerEventDispatcher.dispose(); setRotationAnimation(null); + // Unlink death from remote to clear the reference from binder -> mRemoteInsetsDeath + // -> this DisplayContent. + setRemoteInsetsController(null); mWmService.mAnimator.removeDisplayLocked(mDisplayId); mOverlayLayer.release(); + mWindowingLayer.release(); mInputMonitor.onDisplayRemoved(); mWmService.mDisplayNotificationController.dispatchDisplayRemoved(this); mWmService.mAccessibilityController.onDisplayRemoved(mDisplayId); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 18b0e3311a94..522a6c19c6be 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -1433,6 +1433,13 @@ class Task extends TaskFragment { final TaskFragment childTaskFrag = child.asTaskFragment(); if (childTaskFrag != null && childTaskFrag.asTask() == null) { childTaskFrag.setMinDimensions(mMinWidth, mMinHeight); + + // The starting window should keep covering its task when a pure TaskFragment is added + // because its bounds may not fill the task. + final ActivityRecord top = getTopMostActivity(); + if (top != null) { + top.associateStartingWindowWithTaskIfNeeded(); + } } } diff --git a/services/core/xsd/display-device-config/autobrightness.xsd b/services/core/xsd/display-device-config/autobrightness.xsd new file mode 100644 index 000000000000..477625a36cbd --- /dev/null +++ b/services/core/xsd/display-device-config/autobrightness.xsd @@ -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. +--> +<xs:schema version="2.0" + elementFormDefault="qualified" + xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:complexType name="autoBrightness"> + <xs:sequence> + <!-- Sets the debounce for autoBrightness brightening in millis--> + <xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger" + minOccurs="0" maxOccurs="1"> + <xs:annotation name="final"/> + </xs:element> + <!-- Sets the debounce for autoBrightness darkening in millis--> + <xs:element name="darkeningLightDebounceMillis" type="xs:nonNegativeInteger" + minOccurs="0" maxOccurs="1"> + <xs:annotation name="final"/> + </xs:element> + </xs:sequence> + </xs:complexType> +</xs:schema>
\ No newline at end of file diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index 98f83d8c0d09..bea5e2c2de74 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -23,6 +23,7 @@ <xs:schema version="2.0" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:include schemaLocation="autobrightness.xsd" /> <xs:element name="displayConfiguration"> <xs:complexType> <xs:sequence> @@ -342,74 +343,4 @@ <xs:annotation name="final"/> </xs:element> </xs:complexType> - - <xs:complexType name="autoBrightness"> - <xs:sequence> - <!-- Sets the debounce for autoBrightness brightening in millis--> - <xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger" - minOccurs="0" maxOccurs="1"> - <xs:annotation name="final"/> - </xs:element> - <!-- Sets the debounce for autoBrightness darkening in millis--> - <xs:element name="darkeningLightDebounceMillis" type="xs:nonNegativeInteger" - minOccurs="0" maxOccurs="1"> - <xs:annotation name="final"/> - </xs:element> - <!-- Sets the brightness mapping of the desired screen brightness in nits to the - corresponding lux for the current display --> - <xs:element name="displayBrightnessMapping" type="displayBrightnessMapping" - minOccurs="0" maxOccurs="1"> - <xs:annotation name="final"/> - </xs:element> - </xs:sequence> - </xs:complexType> - - <!-- Represents the brightness mapping of the desired screen brightness in nits to the - corresponding lux for the current display --> - <xs:complexType name="displayBrightnessMapping"> - <xs:sequence> - <!-- Sets the list of display brightness points, each representing the desired screen - brightness in nits to the corresponding lux for the current display - - The N entries of this array define N + 1 control points as follows: - (1-based arrays) - - Point 1: (0, nits[1]): currentLux <= 0 - Point 2: (lux[1], nits[2]): 0 < currentLux <= lux[1] - Point 3: (lux[2], nits[3]): lux[2] < currentLux <= lux[3] - ... - Point N+1: (lux[N], nits[N+1]): lux[N] < currentLux - - The control points must be strictly increasing. Each control point - corresponds to an entry in the brightness backlight values arrays. - For example, if currentLux == lux[1] (first element of the levels array) - then the brightness will be determined by nits[2] (second element - of the brightness values array). - --> - <xs:element name="displayBrightnessPoint" type="displayBrightnessPoint" - minOccurs="1" maxOccurs="unbounded"> - <xs:annotation name="final"/> - </xs:element> - </xs:sequence> - </xs:complexType> - - <!-- Represents a point in the display brightness mapping, representing the lux level from the - light sensor to the desired screen brightness in nits at this level --> - <xs:complexType name="displayBrightnessPoint"> - <xs:sequence> - <!-- The lux level from the light sensor. This must be a non-negative integer --> - <xs:element name="lux" type="xs:nonNegativeInteger" - minOccurs="1" maxOccurs="1"> - <xs:annotation name="final"/> - </xs:element> - - <!-- Desired screen brightness in nits corresponding to the suggested lux values. - The display brightness is defined as the measured brightness of an all-white image. - This must be a non-negative integer --> - <xs:element name="nits" type="xs:nonNegativeInteger" - minOccurs="1" maxOccurs="1"> - <xs:annotation name="final"/> - </xs:element> - </xs:sequence> - </xs:complexType> </xs:schema> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index e5d26177b725..e9a926946764 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -5,10 +5,8 @@ package com.android.server.display.config { ctor public AutoBrightness(); method public final java.math.BigInteger getBrighteningLightDebounceMillis(); method public final java.math.BigInteger getDarkeningLightDebounceMillis(); - method public final com.android.server.display.config.DisplayBrightnessMapping getDisplayBrightnessMapping(); method public final void setBrighteningLightDebounceMillis(java.math.BigInteger); method public final void setDarkeningLightDebounceMillis(java.math.BigInteger); - method public final void setDisplayBrightnessMapping(com.android.server.display.config.DisplayBrightnessMapping); } public class BrightnessThresholds { @@ -45,19 +43,6 @@ package com.android.server.display.config { method public java.util.List<com.android.server.display.config.Density> getDensity(); } - public class DisplayBrightnessMapping { - ctor public DisplayBrightnessMapping(); - method public final java.util.List<com.android.server.display.config.DisplayBrightnessPoint> getDisplayBrightnessPoint(); - } - - public class DisplayBrightnessPoint { - ctor public DisplayBrightnessPoint(); - method public final java.math.BigInteger getLux(); - method public final java.math.BigInteger getNits(); - method public final void setLux(java.math.BigInteger); - method public final void setNits(java.math.BigInteger); - } - public class DisplayConfiguration { ctor public DisplayConfiguration(); method @NonNull public final com.android.server.display.config.Thresholds getAmbientBrightnessChangeThresholds(); diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java index 220cd890e045..617321beadd2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -149,12 +149,6 @@ public class LocalDisplayAdapterTest { .thenReturn(mockArray); when(mMockedResources.obtainTypedArray(R.array.config_roundedCornerBottomRadiusArray)) .thenReturn(mockArray); - when(mMockedResources.obtainTypedArray( - com.android.internal.R.array.config_autoBrightnessDisplayValuesNits)) - .thenReturn(mockArray); - when(mMockedResources.obtainTypedArray( - com.android.internal.R.array.config_autoBrightnessLevels)) - .thenReturn(mockArray); } @After diff --git a/services/tests/servicestests/src/com/android/server/ambientcontext/AmbientContextManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/ambientcontext/AmbientContextManagerServiceTest.java new file mode 100644 index 000000000000..6bb494d3a92a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/ambientcontext/AmbientContextManagerServiceTest.java @@ -0,0 +1,64 @@ +/* + * 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.server.ambientcontext; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.PendingIntent; +import android.app.ambientcontext.AmbientContextEvent; +import android.app.ambientcontext.AmbientContextEventRequest; +import android.content.Intent; +import android.os.RemoteCallback; +import android.os.UserHandle; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import org.junit.Test; + +/** + * Unit test for {@link AmbientContextManagerService}. + * atest FrameworksServicesTests:AmbientContextManagerServiceTest + */ +public class AmbientContextManagerServiceTest { + public static final String SYSTEM_PACKAGE_NAME = "com.android.frameworks.servicestests"; + private static final int USER_ID = UserHandle.USER_SYSTEM; + + @SmallTest + @Test + public void testClientRequest() { + AmbientContextEventRequest request = new AmbientContextEventRequest.Builder() + .addEventType(AmbientContextEvent.EVENT_COUGH) + .build(); + Intent intent = new Intent(); + PendingIntent pendingIntent = PendingIntent.getBroadcast( + InstrumentationRegistry.getTargetContext(), 0, + intent, PendingIntent.FLAG_IMMUTABLE); + AmbientContextManagerService.ClientRequest clientRequest = + new AmbientContextManagerService.ClientRequest(USER_ID, request, + pendingIntent, new RemoteCallback(result -> {})); + + assertThat(clientRequest.getRequest()).isEqualTo(request); + assertThat(clientRequest.getPackageName()).isEqualTo(SYSTEM_PACKAGE_NAME); + assertThat(clientRequest.hasUserId(USER_ID)).isTrue(); + assertThat(clientRequest.hasUserId(-1)).isFalse(); + assertThat(clientRequest.hasUserIdAndPackageName(USER_ID, SYSTEM_PACKAGE_NAME)).isTrue(); + assertThat(clientRequest.hasUserIdAndPackageName(-1, SYSTEM_PACKAGE_NAME)).isFalse(); + assertThat(clientRequest.hasUserIdAndPackageName(USER_ID, "random.package.name")) + .isFalse(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/ambientcontext/OWNERS b/services/tests/servicestests/src/com/android/server/ambientcontext/OWNERS new file mode 100644 index 000000000000..ddfb6e39fd3a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/ambientcontext/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/ambientcontext/OWNERS diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java index 261b882319d8..03ea6137074d 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -19,19 +19,16 @@ package com.android.server.display; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.anyFloat; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; -import android.content.res.TypedArray; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -55,16 +52,22 @@ public final class DisplayDeviceConfigTest { private Resources mResources; @Before - public void setUp() { + public void setUp() throws IOException { MockitoAnnotations.initMocks(this); when(mContext.getResources()).thenReturn(mResources); mockDeviceConfigs(); + try { + Path tempFile = Files.createTempFile("display_config", ".tmp"); + Files.write(tempFile, getContent().getBytes(StandardCharsets.UTF_8)); + mDisplayDeviceConfig = new DisplayDeviceConfig(mContext); + mDisplayDeviceConfig.initFromFile(tempFile.toFile()); + } catch (IOException e) { + throw new IOException("Failed to setup the display device config.", e); + } } @Test - public void testConfigValuesFromDisplayConfig() throws IOException { - setupDisplayDeviceConfigFromDisplayConfigFile(); - + public void testConfigValues() { assertEquals(mDisplayDeviceConfig.getAmbientHorizonLong(), 5000); assertEquals(mDisplayDeviceConfig.getAmbientHorizonShort(), 50); assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000); @@ -85,24 +88,10 @@ public final class DisplayDeviceConfigTest { assertEquals(mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), 0.002, 0.000001f); assertEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLightDebounce(), 2000); assertEquals(mDisplayDeviceConfig.getAutoBrightnessDarkeningLightDebounce(), 1000); - assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new - float[]{50.0f, 80.0f}, 0.0f); - assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new - float[]{45.0f, 75.0f}, 0.0f); - // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping, - // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor. - } - @Test - public void testConfigValuesFromDeviceConfig() { - setupDisplayDeviceConfigFromDeviceConfigFile(); - assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(), new - float[]{0.0f, 110.0f, 500.0f}, 0.0f); - assertArrayEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsNits(), new - float[]{2.0f, 200.0f, 600.0f}, 0.0f); // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping, // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor. - + // Also add test for the case where optional display configs are null } private String getContent() { @@ -125,16 +114,6 @@ public final class DisplayDeviceConfigTest { + "<autoBrightness>\n" + "<brighteningLightDebounceMillis>2000</brighteningLightDebounceMillis>\n" + "<darkeningLightDebounceMillis>1000</darkeningLightDebounceMillis>\n" - + "<displayBrightnessMapping>\n" - + "<displayBrightnessPoint>\n" - + "<lux>50</lux>\n" - + "<nits>45</nits>\n" - + "</displayBrightnessPoint>\n" - + "<displayBrightnessPoint>\n" - + "<lux>80</lux>\n" - + "<nits>75</nits>\n" - + "</displayBrightnessPoint>\n" - + "</displayBrightnessMapping>\n" + "</autoBrightness>\n" + "<highBrightnessMode enabled=\"true\">\n" + "<transitionPoint>0.62</transitionPoint>\n" @@ -206,64 +185,4 @@ public final class DisplayDeviceConfigTest { when(mResources.getFloat(com.android.internal.R.dimen .config_screenBrightnessSettingMaximumFloat)).thenReturn(1.0f); } - - private void setupDisplayDeviceConfigFromDisplayConfigFile() throws IOException { - Path tempFile = Files.createTempFile("display_config", ".tmp"); - Files.write(tempFile, getContent().getBytes(StandardCharsets.UTF_8)); - mDisplayDeviceConfig = new DisplayDeviceConfig(mContext); - mDisplayDeviceConfig.initFromFile(tempFile.toFile()); - } - - private void setupDisplayDeviceConfigFromDeviceConfigFile() { - TypedArray screenBrightnessNits = createFloatTypedArray(new float[]{2.0f, 250.0f, 650.0f}); - when(mResources.obtainTypedArray( - com.android.internal.R.array.config_screenBrightnessNits)) - .thenReturn(screenBrightnessNits); - TypedArray screenBrightnessBacklight = createFloatTypedArray(new - float[]{0.0f, 120.0f, 255.0f}); - when(mResources.obtainTypedArray( - com.android.internal.R.array.config_screenBrightnessBacklight)) - .thenReturn(screenBrightnessBacklight); - when(mResources.getIntArray(com.android.internal.R.array - .config_screenBrightnessBacklight)).thenReturn(new int[]{0, 120, 255}); - - when(mResources.getIntArray(com.android.internal.R.array - .config_autoBrightnessLevels)).thenReturn(new int[]{30, 80}); - when(mResources.getIntArray(com.android.internal.R.array - .config_autoBrightnessDisplayValuesNits)).thenReturn(new int[]{25, 55}); - - TypedArray screenBrightnessLevelNits = createFloatTypedArray(new - float[]{2.0f, 200.0f, 600.0f}); - when(mResources.obtainTypedArray( - com.android.internal.R.array.config_autoBrightnessDisplayValuesNits)) - .thenReturn(screenBrightnessLevelNits); - TypedArray screenBrightnessLevelLux = createFloatTypedArray(new - float[]{0.0f, 110.0f, 500.0f}); - when(mResources.obtainTypedArray( - com.android.internal.R.array.config_autoBrightnessLevels)) - .thenReturn(screenBrightnessLevelLux); - - mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true); - - } - - private TypedArray createFloatTypedArray(float[] vals) { - TypedArray mockArray = mock(TypedArray.class); - when(mockArray.length()).thenAnswer(invocation -> { - return vals.length; - }); - when(mockArray.getFloat(anyInt(), anyFloat())).thenAnswer(invocation -> { - final float def = (float) invocation.getArguments()[1]; - if (vals == null) { - return def; - } - int idx = (int) invocation.getArguments()[0]; - if (idx >= 0 && idx < vals.length) { - return vals[idx]; - } else { - return def; - } - }); - return mockArray; - } } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index a8b864bab553..eb61a9ce57a5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2871,6 +2871,7 @@ public class ActivityRecordTests extends WindowTestsBase { mAtm, null /* fragmentToken */, false /* createdByOrganizer */); fragmentSetup.accept(taskFragment1, new Rect(0, 0, width / 2, height)); task.addChild(taskFragment1, POSITION_TOP); + assertEquals(task, activity1.mStartingData.mAssociatedTask); final TaskFragment taskFragment2 = new TaskFragment( mAtm, null /* fragmentToken */, false /* createdByOrganizer */); @@ -2892,7 +2893,6 @@ public class ActivityRecordTests extends WindowTestsBase { eq(task.mSurfaceControl)); assertEquals(activity1.mStartingData, startingWindow.mStartingData); assertEquals(task.mSurfaceControl, startingWindow.getAnimationLeashParent()); - assertEquals(task, activity1.mStartingData.mAssociatedTask); assertEquals(taskFragment1.getBounds(), activity1.getBounds()); // The activity was resized by task fragment, but starting window must still cover the task. assertEquals(taskBounds, activity1.mStartingWindow.getBounds()); @@ -2900,7 +2900,6 @@ public class ActivityRecordTests extends WindowTestsBase { // The starting window is only removed when all embedded activities are drawn. final WindowState activityWindow = mock(WindowState.class); activity1.onFirstWindowDrawn(activityWindow); - assertNotNull(activity1.mStartingWindow); activity2.onFirstWindowDrawn(activityWindow); assertNull(activity1.mStartingWindow); } |