diff options
87 files changed, 1916 insertions, 858 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 51585af10f5d..03fa8413f2cc 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11078,21 +11078,12 @@ public final class Settings { "assist_long_press_home_enabled"; /** - * Whether press and hold on nav handle can trigger search. + * Whether all entrypoints can trigger search. Replaces individual settings. * * @hide */ - public static final String SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED = - "search_press_hold_nav_handle_enabled"; - - /** - * Whether long-pressing on the home button can trigger search. - * - * @hide - */ - public static final String SEARCH_LONG_PRESS_HOME_ENABLED = - "search_long_press_home_enabled"; - + public static final String SEARCH_ALL_ENTRYPOINTS_ENABLED = + "search_all_entrypoints_enabled"; /** * Whether or not the accessibility data streaming is enbled for the diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 14fb17c09031..65bf24179bea 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -38,6 +38,17 @@ flag { } flag { + name: "skip_sleeping_when_switching_display" + namespace: "windowing_frontend" + description: "Reduce unnecessary visibility or lifecycle changes when changing fold state" + bug: "303241079" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "introduce_smoother_dimmer" namespace: "windowing_frontend" description: "Refactor dim to fix flickers" diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java index 5e89d06facd1..e1aff2af0965 100644 --- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java +++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java @@ -3281,6 +3281,11 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } public PackageImpl(Parcel in) { + this(in, /* callback */ null); + } + + public PackageImpl(@NonNull Parcel in, @Nullable ParsingPackageUtils.Callback callback) { + mCallback = callback; // We use the boot classloader for all classes that we load. final ClassLoader boot = Object.class.getClassLoader(); this.supportsSmallScreens = sForBoolean.unparcel(in); diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 763d9ce1a053..6b0c2d28b776 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -143,9 +143,11 @@ message SecureSettingsProto { optional SettingProto gesture_setup_complete = 9 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto touch_gesture_enabled = 10 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto long_press_home_enabled = 11 [ (android.privacy).dest = DEST_AUTOMATIC ]; - optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC ]; - optional SettingProto search_long_press_home_enabled = 13 [ (android.privacy).dest = DEST_AUTOMATIC ]; + // Deprecated - use search_all_entrypoints_enabled instead + optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC, deprecated = true ]; + optional SettingProto search_long_press_home_enabled = 13 [ (android.privacy).dest = DEST_AUTOMATIC, deprecated = true ]; optional SettingProto visual_query_accessibility_detection_enabled = 14 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto search_all_entrypoints_enabled = 15 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Assist assist = 7; diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 8edf42afa730..6f605e94c1eb 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6422,10 +6422,8 @@ <!-- Default value for Settings.ASSIST_TOUCH_GESTURE_ENABLED --> <bool name="config_assistTouchGestureEnabledDefault">true</bool> - <!-- Default value for Settings.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED --> - <bool name="config_searchPressHoldNavHandleEnabledDefault">true</bool> - <!-- Default value for Settings.ASSIST_LONG_PRESS_HOME_ENABLED for search overlay --> - <bool name="config_searchLongPressHomeEnabledDefault">true</bool> + <!-- Default value for Settings.SEARCH_ALL_ENTRYPOINTS_ENABLED --> + <bool name="config_searchAllEntrypointsEnabledDefault">true</bool> <!-- The maximum byte size of the information contained in the bundle of HotwordDetectedResult. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index a1804672da73..e9331bf938db 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5018,8 +5018,7 @@ <java-symbol type="bool" name="config_assistLongPressHomeEnabledDefault" /> <java-symbol type="bool" name="config_assistTouchGestureEnabledDefault" /> - <java-symbol type="bool" name="config_searchPressHoldNavHandleEnabledDefault" /> - <java-symbol type="bool" name="config_searchLongPressHomeEnabledDefault" /> + <java-symbol type="bool" name="config_searchAllEntrypointsEnabledDefault" /> <java-symbol type="integer" name="config_hotwordDetectedResultMaxBundleSize" /> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 87e372cc304c..bd186ba22588 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -1368,7 +1368,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * Handles all changes to the PictureInPictureParams. */ protected void applyNewPictureInPictureParams(@NonNull PictureInPictureParams params) { - if (mDeferredTaskInfo != null || PipUtils.aspectRatioChanged(params.getAspectRatioFloat(), + if (PipUtils.aspectRatioChanged(params.getAspectRatioFloat(), mPictureInPictureParams.getAspectRatioFloat())) { if (mPipBoundsAlgorithm.isValidPictureInPictureAspectRatio( params.getAspectRatioFloat())) { @@ -1381,8 +1381,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, TAG, params.hasSetAspectRatio(), params.getAspectRatioFloat()); } } - if (mDeferredTaskInfo != null - || PipUtils.remoteActionsChanged(params.getActions(), + if (PipUtils.remoteActionsChanged(params.getActions(), mPictureInPictureParams.getActions()) || !PipUtils.remoteActionsMatch(params.getCloseAction(), mPictureInPictureParams.getCloseAction())) { diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 2fa1c6eda458..2d4a4a1ebb44 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -255,8 +255,7 @@ public class SecureSettings { Settings.Secure.HEARING_AID_MEDIA_ROUTING, Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING, Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, - Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, - Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED, + Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, Settings.Secure.HUB_MODE_TUTORIAL_STATE, Settings.Secure.STYLUS_BUTTONS_ENABLED, Settings.Secure.STYLUS_HANDWRITING_ENABLED, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 2535fdb6e4d0..24c8c3dfa994 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -208,8 +208,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.ASSIST_TOUCH_GESTURE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ASSIST_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, BOOLEAN_VALIDATOR); - VALIDATORS.put(Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, BOOLEAN_VALIDATOR); - VALIDATORS.put(Secure.SEARCH_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VR_DISPLAY_MODE, new DiscreteValueValidator(new String[] {"0", "1"})); VALIDATORS.put(Secure.NOTIFICATION_BADGING, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.NOTIFICATION_DISMISS_RTL, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 02d212cb4996..dba3bac4a4b8 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1950,11 +1950,8 @@ class SettingsProtoDumpUtil { Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED, SecureSettingsProto.Assist.LONG_PRESS_HOME_ENABLED); dumpSetting(s, p, - Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, - SecureSettingsProto.Assist.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED); - dumpSetting(s, p, - Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED, - SecureSettingsProto.Assist.SEARCH_LONG_PRESS_HOME_ENABLED); + Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, + SecureSettingsProto.Assist.SEARCH_ALL_ENTRYPOINTS_ENABLED); dumpSetting(s, p, Settings.Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, SecureSettingsProto.Assist.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED); diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 12e8f574e906..41d9a2313435 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -430,7 +430,7 @@ </intent-filter> </receiver> - <activity android:name=".screenshot.LongScreenshotActivity" + <activity android:name=".screenshot.scroll.LongScreenshotActivity" android:theme="@style/LongScreenshotActivity" android:process=":screenshot" android:exported="false" @@ -525,15 +525,6 @@ </intent-filter> </activity-alias> - <!-- Springboard for launching the share and edit activity. This needs to be in the main - system ui process since we need to notify the status bar to dismiss the keyguard --> - <receiver android:name=".screenshot.ActionProxyReceiver" - android:exported="false" /> - - <!-- Callback for deleting screenshot notification --> - <receiver android:name=".screenshot.DeleteScreenshotReceiver" - android:exported="false" /> - <!-- Callback for invoking a smart action from the screenshot notification. --> <receiver android:name=".screenshot.SmartActionsReceiver" android:exported="false"/> diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 8c8975fdbdd5..dd1fc234af20 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -405,6 +405,13 @@ flag { } flag { + name: "screenshot_shelf_ui" + namespace: "systemui" + description: "Use new shelf UI flow for screenshots" + bug: "329659738" +} + +flag { name: "run_fingerprint_detect_on_dismissible_keyguard" namespace: "systemui" description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible." diff --git a/packages/SystemUI/res/layout/app_clips_screenshot.xml b/packages/SystemUI/res/layout/app_clips_screenshot.xml index cb638ee87834..bcc7bca8c915 100644 --- a/packages/SystemUI/res/layout/app_clips_screenshot.xml +++ b/packages/SystemUI/res/layout/app_clips_screenshot.xml @@ -69,7 +69,7 @@ tools:minHeight="100dp" tools:minWidth="100dp" /> - <com.android.systemui.screenshot.CropView + <com.android.systemui.screenshot.scroll.CropView android:id="@+id/crop_view" android:layout_width="0px" android:layout_height="0px" diff --git a/packages/SystemUI/res/layout/long_screenshot.xml b/packages/SystemUI/res/layout/long_screenshot.xml index 8a19c2ebdcd6..4d207da851cd 100644 --- a/packages/SystemUI/res/layout/long_screenshot.xml +++ b/packages/SystemUI/res/layout/long_screenshot.xml @@ -100,7 +100,7 @@ app:layout_constraintStart_toStartOf="parent" android:transitionName="screenshot_preview_image"/> - <com.android.systemui.screenshot.CropView + <com.android.systemui.screenshot.scroll.CropView android:id="@+id/crop_view" android:layout_width="0px" android:layout_height="0px" @@ -122,7 +122,7 @@ tools:minHeight="100dp" tools:minWidth="100dp" /> - <com.android.systemui.screenshot.MagnifierView + <com.android.systemui.screenshot.scroll.MagnifierView android:id="@+id/magnifier" android:visibility="invisible" android:layout_width="200dp" diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml new file mode 100644 index 000000000000..ef1a21f2fdf6 --- /dev/null +++ b/packages/SystemUI/res/layout/screenshot_shelf.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.systemui.screenshot.ui.ScreenshotShelfView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <ImageView + android:id="@+id/actions_container_background" + android:visibility="gone" + android:layout_height="0dp" + android:layout_width="0dp" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/actions_container" + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toTopOf="@id/guideline"/> + <HorizontalScrollView + android:id="@+id/actions_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" + android:paddingEnd="@dimen/overlay_action_container_padding_end" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:scrollbars="none" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_percent="1.0" + app:layout_constraintWidth_max="wrap" + app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> + <LinearLayout + android:id="@+id/screenshot_actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_share_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_edit_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_scroll_chip" + android:visibility="gone" /> + </LinearLayout> + </HorizontalScrollView> + <View + android:id="@+id/screenshot_preview_border" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="16dp" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="14dp" + android:elevation="8dp" + android:background="@drawable/overlay_border" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintBottom_toBottomOf="parent"/> + <ImageView + android:id="@+id/screenshot_preview" + android:layout_width="@dimen/overlay_x_scale" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" + android:layout_gravity="center" + android:elevation="8dp" + android:contentDescription="@string/screenshot_edit_description" + android:scaleType="fitEnd" + android:background="@drawable/overlay_preview_background" + android:adjustViewBounds="true" + android:clickable="true" + app:layout_constraintStart_toStartOf="@id/screenshot_preview_border" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/> + <ImageView + android:id="@+id/screenshot_badge" + android:layout_width="56dp" + android:layout_height="56dp" + android:visibility="gone" + android:elevation="9dp" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/> + <FrameLayout + android:id="@+id/screenshot_dismiss_button" + android:layout_width="@dimen/overlay_dismiss_button_tappable_size" + android:layout_height="@dimen/overlay_dismiss_button_tappable_size" + android:elevation="11dp" + android:visibility="gone" + app:layout_constraintStart_toEndOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintBottom_toTopOf="@id/screenshot_preview" + android:contentDescription="@string/screenshot_dismiss_description"> + <ImageView + android:id="@+id/screenshot_dismiss_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="@dimen/overlay_dismiss_button_margin" + android:background="@drawable/circular_background" + android:backgroundTint="?androidprv:attr/materialColorPrimary" + android:tint="?androidprv:attr/materialColorOnPrimary" + android:padding="4dp" + android:src="@drawable/ic_close"/> + </FrameLayout> + <ImageView + android:id="@+id/screenshot_scrollable_preview" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="matrix" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/screenshot_preview" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + android:elevation="7dp"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="0dp" /> + + <FrameLayout + android:id="@+id/screenshot_message_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginTop="4dp" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + android:paddingHorizontal="@dimen/overlay_action_container_padding_end" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/guideline" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintWidth_max="450dp" + app:layout_constraintHorizontal_bias="0"> + <include layout="@layout/screenshot_work_profile_first_run" /> + <include layout="@layout/screenshot_detection_notice" /> + </FrameLayout> +</com.android.systemui.screenshot.ui.ScreenshotShelfView> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index b71341791ee6..ef9235bd1a50 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -230,6 +230,8 @@ <string name="screenshot_edit_label">Edit</string> <!-- Content description indicating that tapping the element will allow editing the screenshot [CHAR LIMIT=NONE] --> <string name="screenshot_edit_description">Edit screenshot</string> + <!-- Label for UI element which allows sharing the screenshot [CHAR LIMIT=30] --> + <string name="screenshot_share_label">Share</string> <!-- Content description indicating that tapping the element will allow sharing the screenshot [CHAR LIMIT=NONE] --> <string name="screenshot_share_description">Share screenshot</string> <!-- Label for UI element which allows the user to capture additional off-screen content in a screenshot. [CHAR LIMIT=30] --> diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index f28d4052b5a8..8a2245d3d14c 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -404,7 +404,9 @@ constructor( if (nextAlarmMillis > 0) nextAlarmMillis else null, SysuiR.string::status_bar_alarm.name ) - .also { data -> clock?.run { events.onAlarmDataChanged(data) } } + .also { data -> + mainExecutor.execute { clock?.run { events.onAlarmDataChanged(data) } } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt index 8c87b0c78ea7..893887fad176 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt @@ -93,6 +93,8 @@ constructor( val keyguardAuthenticatedPrimaryAuth: Flow<Int> = repository.keyguardAuthenticatedPrimaryAuth val keyguardAuthenticatedBiometrics: Flow<Boolean> = repository.keyguardAuthenticatedBiometrics.filterNotNull() + val keyguardAuthenticatedBiometricsHandled: Flow<Unit> = + repository.keyguardAuthenticatedBiometrics.filter { it == null }.map {} val userRequestedBouncerWhenAlreadyAuthenticated: Flow<Int> = repository.userRequestedBouncerWhenAlreadyAuthenticated.filterNotNull() val isShowing: StateFlow<Boolean> = repository.primaryBouncerShow diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModel.kt index 1c9d1f01e89e..e1fea5f9c62a 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModel.kt @@ -23,12 +23,14 @@ import com.android.systemui.bouncer.shared.model.BouncerShowMessageModel import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.bouncer.ui.BouncerViewDelegate import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge /** Models UI state for the lock screen bouncer; handles user input. */ +@ExperimentalCoroutinesApi class KeyguardBouncerViewModel @Inject constructor( diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java index 9afd5ede0b4c..d2df276002cc 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java @@ -24,9 +24,9 @@ import com.android.systemui.contrast.ContrastDialogActivity; import com.android.systemui.keyguard.WorkLockActivity; import com.android.systemui.people.PeopleSpaceActivity; import com.android.systemui.people.widget.LaunchConversationActivity; -import com.android.systemui.screenshot.LongScreenshotActivity; import com.android.systemui.screenshot.appclips.AppClipsActivity; import com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity; +import com.android.systemui.screenshot.scroll.LongScreenshotActivity; import com.android.systemui.sensorprivacy.SensorUseStartedActivity; import com.android.systemui.settings.brightness.BrightnessDialog; import com.android.systemui.telephony.ui.activity.SwitchToManagedProfileForCallActivity; diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java index 3d8e4cb71aca..6aa5e8b6d098 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java @@ -22,8 +22,6 @@ import com.android.systemui.GuestResetOrExitSessionReceiver; import com.android.systemui.media.dialog.MediaOutputDialogReceiver; import com.android.systemui.people.widget.PeopleSpaceWidgetPinnedReceiver; 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; @@ -42,24 +40,6 @@ public abstract class DefaultBroadcastReceiverBinder { */ @Binds @IntoMap - @ClassKey(ActionProxyReceiver.class) - public abstract BroadcastReceiver bindActionProxyReceiver( - ActionProxyReceiver broadcastReceiver); - - /** - * - */ - @Binds - @IntoMap - @ClassKey(DeleteScreenshotReceiver.class) - public abstract BroadcastReceiver bindDeleteScreenshotReceiver( - DeleteScreenshotReceiver broadcastReceiver); - - /** - * - */ - @Binds - @IntoMap @ClassKey(SmartActionsReceiver.class) public abstract BroadcastReceiver bindSmartActionsReceiver( SmartActionsReceiver broadcastReceiver); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt index d40021007a2b..3a2781c81f7c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt @@ -102,9 +102,16 @@ object AlternateBouncerViewBinder { launch { viewModel.scrimAlpha.collect { + val wasVisible = alternateBouncerViewContainer.visibility == View.VISIBLE alternateBouncerViewContainer.visibility = if (it < .1f) View.INVISIBLE else View.VISIBLE scrim.viewAlpha = it + if ( + wasVisible && alternateBouncerViewContainer.visibility == View.INVISIBLE + ) { + // view is no longer visible + viewModel.hideAlternateBouncer() + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt index 2526f0aec796..10a9e3bba7f0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt @@ -89,4 +89,8 @@ constructor( fun showPrimaryBouncer() { statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true) } + + fun hideAlternateBouncer() { + statusBarKeyguardViewManager.hideAlternateBouncer(false) + } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java index dfe41eb9f7f2..d49a513f6e9f 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java @@ -243,7 +243,7 @@ public final class NavBarHelper implements Settings.Secure.getUriFor(Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED), false, mAssistContentObserver, UserHandle.USER_ALL); mContentResolver.registerContentObserver( - Settings.Secure.getUriFor(Secure.SEARCH_LONG_PRESS_HOME_ENABLED), + Settings.Secure.getUriFor(Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED), false, mAssistContentObserver, UserHandle.USER_ALL); mContentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ASSIST_TOUCH_GESTURE_ENABLED), @@ -443,10 +443,10 @@ public final class NavBarHelper implements boolean overrideLongPressHome = mAssistManagerLazy.get() .shouldOverrideAssist(AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); boolean longPressDefault = mContext.getResources().getBoolean(overrideLongPressHome - ? com.android.internal.R.bool.config_searchLongPressHomeEnabledDefault + ? com.android.internal.R.bool.config_searchAllEntrypointsEnabledDefault : com.android.internal.R.bool.config_assistLongPressHomeEnabledDefault); mLongPressHomeEnabled = Settings.Secure.getIntForUser(mContentResolver, - overrideLongPressHome ? Secure.SEARCH_LONG_PRESS_HOME_ENABLED + overrideLongPressHome ? Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED : Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED, longPressDefault ? 1 : 0, mUserTracker.getUserId()) != 0; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java deleted file mode 100644 index 7e234aeed0aa..000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.screenshot; - -import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_EDIT; -import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_SHARE; -import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ACTION_INTENT; -import static com.android.systemui.screenshot.ScreenshotController.EXTRA_DISALLOW_ENTER_PIP; -import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; -import static com.android.systemui.screenshot.ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED; -import static com.android.systemui.statusbar.phone.CentralSurfaces.SYSTEM_DIALOG_REASON_SCREENSHOT; - -import android.app.ActivityOptions; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import android.view.RemoteAnimationAdapter; -import android.view.WindowManagerGlobal; - -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.settings.DisplayTracker; -import com.android.systemui.shared.system.ActivityManagerWrapper; - -import javax.inject.Inject; - -/** - * Receiver to proxy the share or edit intent, used to clean up the notification and send - * appropriate signals to the system (ie. to dismiss the keyguard if necessary). - */ -public class ActionProxyReceiver extends BroadcastReceiver { - private static final String TAG = "ActionProxyReceiver"; - - private final ActivityManagerWrapper mActivityManagerWrapper; - private final ScreenshotSmartActions mScreenshotSmartActions; - private final DisplayTracker mDisplayTracker; - private final ActivityStarter mActivityStarter; - - @Inject - public ActionProxyReceiver(ActivityManagerWrapper activityManagerWrapper, - ScreenshotSmartActions screenshotSmartActions, - DisplayTracker displayTracker, - ActivityStarter activityStarter) { - mActivityManagerWrapper = activityManagerWrapper; - mScreenshotSmartActions = screenshotSmartActions; - mDisplayTracker = displayTracker; - mActivityStarter = activityStarter; - } - - @Override - public void onReceive(Context context, final Intent intent) { - Runnable startActivityRunnable = () -> { - mActivityManagerWrapper.closeSystemWindows(SYSTEM_DIALOG_REASON_SCREENSHOT); - - PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT); - ActivityOptions opts = ActivityOptions.makeBasic(); - opts.setDisallowEnterPictureInPictureWhileLaunching( - intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false)); - opts.setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); - try { - actionIntent.send(context, 0, null, null, null, null, opts.toBundle()); - if (intent.getBooleanExtra(ScreenshotController.EXTRA_OVERRIDE_TRANSITION, false)) { - RemoteAnimationAdapter runner = new RemoteAnimationAdapter( - ScreenshotController.SCREENSHOT_REMOTE_RUNNER, 0, 0); - try { - WindowManagerGlobal.getWindowManagerService() - .overridePendingAppTransitionRemote(runner, - mDisplayTracker.getDefaultDisplayId()); - } catch (Exception e) { - Log.e(TAG, "Error overriding screenshot app transition", e); - } - } - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent canceled", e); - } - - }; - - mActivityStarter.executeRunnableDismissingKeyguard(startActivityRunnable, null, - true /* dismissShade */, true /* afterKeyguardGone */, - true /* deferred */); - - if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) { - String actionType = Intent.ACTION_EDIT.equals(intent.getAction()) - ? ACTION_TYPE_EDIT - : ACTION_TYPE_SHARE; - mScreenshotSmartActions.notifyScreenshotAction( - intent.getStringExtra(EXTRA_ID), actionType, false, null); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java deleted file mode 100644 index e0346f2e2a98..000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.screenshot; - -import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_DELETE; -import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; -import static com.android.systemui.screenshot.ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED; -import static com.android.systemui.screenshot.ScreenshotController.SCREENSHOT_URI_ID; - -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; - -import com.android.systemui.dagger.qualifiers.Background; - -import java.util.concurrent.Executor; - -import javax.inject.Inject; - -/** - * Removes the file at a provided URI. - */ -public class DeleteScreenshotReceiver extends BroadcastReceiver { - - private final ScreenshotSmartActions mScreenshotSmartActions; - private final Executor mBackgroundExecutor; - - @Inject - public DeleteScreenshotReceiver(ScreenshotSmartActions screenshotSmartActions, - @Background Executor backgroundExecutor) { - mScreenshotSmartActions = screenshotSmartActions; - mBackgroundExecutor = backgroundExecutor; - } - - @Override - public void onReceive(Context context, Intent intent) { - if (!intent.hasExtra(SCREENSHOT_URI_ID)) { - return; - } - - // And delete the image from the media store - final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID)); - mBackgroundExecutor.execute(() -> { - ContentResolver resolver = context.getContentResolver(); - resolver.delete(uri, null, null); - }); - if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) { - mScreenshotSmartActions.notifyScreenshotAction( - intent.getStringExtra(EXTRA_ID), ACTION_TYPE_DELETE, false, null); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index b42cc98746dc..1c39f71a7d83 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -123,7 +123,7 @@ public class ImageExporter { /** * Writes the given Bitmap to outputFile. */ - ListenableFuture<File> exportToRawFile(Executor executor, Bitmap bitmap, + public ListenableFuture<File> exportToRawFile(Executor executor, Bitmap bitmap, final File outputFile) { return CallbackToFutureAdapter.getFuture( (completer) -> { @@ -165,7 +165,7 @@ public class ImageExporter { * * @return a listenable future result */ - ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, + public ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime, UserHandle owner, int displayId) { final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat, diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt new file mode 100644 index 000000000000..a1481f6d6d2b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.animation.Animator +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.util.Log +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.ScrollCaptureResponse +import android.view.View +import android.view.ViewTreeObserver +import android.view.WindowInsets +import android.window.OnBackInvokedCallback +import android.window.OnBackInvokedDispatcher +import androidx.appcompat.content.res.AppCompatResources +import com.android.internal.logging.UiEventLogger +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.res.R +import com.android.systemui.screenshot.scroll.ScrollCaptureController +import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS +import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Legacy implementation of screenshot view methods. Just proxies the calls down into the original + * ScreenshotView. + */ +class LegacyScreenshotViewProxy +@AssistedInject +constructor( + private val logger: UiEventLogger, + flags: FeatureFlags, + @Assisted private val context: Context, + @Assisted private val displayId: Int +) : ScreenshotViewProxy { + override val view: ScreenshotView = + LayoutInflater.from(context).inflate(R.layout.screenshot, null) as ScreenshotView + override val screenshotPreview: View + override var packageName: String = "" + set(value) { + field = value + view.setPackageName(value) + } + override var callbacks: ScreenshotView.ScreenshotViewCallback? = null + set(value) { + field = value + view.setCallbacks(value) + } + override var screenshot: ScreenshotData? = null + set(value) { + field = value + value?.let { + val badgeBg = + AppCompatResources.getDrawable(context, R.drawable.overlay_badge_background) + val user = it.userHandle + if (badgeBg != null && user != null) { + view.badgeScreenshot(context.packageManager.getUserBadgedIcon(badgeBg, user)) + } + view.setScreenshot(it) + } + } + + override val isAttachedToWindow + get() = view.isAttachedToWindow + override val isDismissing + get() = view.isDismissing + override val isPendingSharedTransition + get() = view.isPendingSharedTransition + + init { + view.setUiEventLogger(logger) + view.setDefaultDisplay(displayId) + view.setFlags(flags) + addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + if (LogConfig.DEBUG_WINDOW) { + Log.d(TAG, "adding OnComputeInternalInsetsListener") + } + view.viewTreeObserver.addOnComputeInternalInsetsListener(view) + screenshotPreview = view.screenshotPreview + } + + override fun reset() = view.reset() + override fun updateInsets(insets: WindowInsets) = view.updateInsets(insets) + override fun updateOrientation(insets: WindowInsets) = view.updateOrientation(insets) + + override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator = + view.createScreenshotDropInAnimation(screenRect, showFlash) + + override fun addQuickShareChip(quickShareAction: Notification.Action) = + view.addQuickShareChip(quickShareAction) + + override fun setChipIntents(imageData: ScreenshotController.SavedImageData) = + view.setChipIntents(imageData) + + override fun requestDismissal(event: ScreenshotEvent) { + if (DEBUG_DISMISS) { + Log.d(TAG, "screenshot dismissal requested") + } + // If we're already animating out, don't restart the animation + if (view.isDismissing) { + if (DEBUG_DISMISS) { + Log.v(TAG, "Already dismissing, ignoring duplicate command $event") + } + return + } + logger.log(event, 0, packageName) + view.animateDismissal() + } + + override fun showScrollChip(packageName: String, onClick: Runnable) = + view.showScrollChip(packageName, onClick) + + override fun hideScrollChip() = view.hideScrollChip() + + override fun prepareScrollingTransition( + response: ScrollCaptureResponse, + screenBitmap: Bitmap, + newScreenshot: Bitmap, + screenshotTakenInPortrait: Boolean, + onTransitionPrepared: Runnable, + ) { + view.prepareScrollingTransition( + response, + screenBitmap, + newScreenshot, + screenshotTakenInPortrait + ) + view.post { onTransitionPrepared.run() } + } + + override fun startLongScreenshotTransition( + transitionDestination: Rect, + onTransitionEnd: Runnable, + longScreenshot: ScrollCaptureController.LongScreenshot + ) = view.startLongScreenshotTransition(transitionDestination, onTransitionEnd, longScreenshot) + + override fun restoreNonScrollingUi() = view.restoreNonScrollingUi() + + override fun stopInputListening() = view.stopInputListening() + + override fun requestFocus() { + view.requestFocus() + } + + override fun announceForAccessibility(string: String) = view.announceForAccessibility(string) + + override fun prepareEntranceAnimation(runnable: Runnable) { + view.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + if (LogConfig.DEBUG_WINDOW) { + Log.d(TAG, "onPreDraw: startAnimation") + } + view.viewTreeObserver.removeOnPreDrawListener(this) + runnable.run() + return true + } + } + ) + } + + private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + val onBackInvokedCallback = OnBackInvokedCallback { + if (LogConfig.DEBUG_INPUT) { + Log.d(TAG, "Predictive Back callback dispatched") + } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + } + view.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + if (LogConfig.DEBUG_INPUT) { + Log.d(TAG, "Registering Predictive Back callback") + } + view + .findOnBackInvokedDispatcher() + ?.registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + onBackInvokedCallback + ) + } + + override fun onViewDetachedFromWindow(view: View) { + if (LogConfig.DEBUG_INPUT) { + Log.d(TAG, "Unregistering Predictive Back callback") + } + view + .findOnBackInvokedDispatcher() + ?.unregisterOnBackInvokedCallback(onBackInvokedCallback) + } + } + ) + } + private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + view.setOnKeyListener( + object : View.OnKeyListener { + override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { + if (LogConfig.DEBUG_INPUT) { + Log.d(TAG, "onKeyEvent: $keyCode") + } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + return true + } + return false + } + } + ) + } + + @AssistedFactory + interface Factory : ScreenshotViewProxy.Factory { + override fun getProxy(context: Context, displayId: Int): LegacyScreenshotViewProxy + } + + companion object { + private const val TAG = "LegacyScreenshotViewProxy" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LogConfig.java b/packages/SystemUI/src/com/android/systemui/screenshot/LogConfig.java index 6050c2b90e34..440cf1c344da 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LogConfig.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LogConfig.java @@ -16,8 +16,9 @@ package com.android.systemui.screenshot; +/** Stores debug log configuration for screenshots. */ @SuppressWarnings("PointlessBooleanExpression") -class LogConfig { +public class LogConfig { /** Log ALL the things... */ private static final boolean DEBUG_ALL = false; @@ -29,36 +30,37 @@ class LogConfig { private static final boolean TAG_WITH_CLASS_NAME = false; /** Action creation and user selection: Share, Save, Edit, Delete, Smart action, etc */ - static final boolean DEBUG_ACTIONS = DEBUG_ALL || false; + public static final boolean DEBUG_ACTIONS = DEBUG_ALL || false; /** Debug info about animations such as start, complete and cancel */ - static final boolean DEBUG_ANIM = DEBUG_ALL || false; + public static final boolean DEBUG_ANIM = DEBUG_ALL || false; /** Whenever Uri is supplied to consumer, or onComplete runnable is run() */ - static final boolean DEBUG_CALLBACK = DEBUG_ALL || false; + public static final boolean DEBUG_CALLBACK = DEBUG_ALL || false; /** Logs information about dismissing the screenshot tool */ - static final boolean DEBUG_DISMISS = DEBUG_ALL || false; + public static final boolean DEBUG_DISMISS = DEBUG_ALL || false; /** Touch or key event driven action or side effects */ - static final boolean DEBUG_INPUT = DEBUG_ALL || false; + public static final boolean DEBUG_INPUT = DEBUG_ALL || false; /** Scroll capture usage */ - static final boolean DEBUG_SCROLL = DEBUG_ALL || false; + public static final boolean DEBUG_SCROLL = DEBUG_ALL || false; /** Service lifecycle events and callbacks */ - static final boolean DEBUG_SERVICE = DEBUG_ALL || false; + public static final boolean DEBUG_SERVICE = DEBUG_ALL || false; /** Storage related actions, Bitmap.compress, ContentManager, etc */ - static final boolean DEBUG_STORAGE = DEBUG_ALL || false; + public static final boolean DEBUG_STORAGE = DEBUG_ALL || false; /** High level logical UI actions: timeout, onConfigChanged, insets, show actions, reset */ - static final boolean DEBUG_UI = DEBUG_ALL || false; + public static final boolean DEBUG_UI = DEBUG_ALL || false; /** Interactions with Window and WindowManager */ - static final boolean DEBUG_WINDOW = DEBUG_ALL || false; + public static final boolean DEBUG_WINDOW = DEBUG_ALL || false; - static String logTag(Class<?> cls) { + /** Get the appropriate class name */ + public static String logTag(Class<?> cls) { return TAG_WITH_CLASS_NAME ? cls.getSimpleName() : TAG_SS; } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt new file mode 100644 index 000000000000..abdbd6880b33 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.UserHandle +import androidx.appcompat.content.res.AppCompatResources +import com.android.systemui.res.R +import javax.inject.Inject + +/** + * Provides static actions for screenshots. This class can be overridden by a vendor-specific SysUI + * implementation. + */ +interface ScreenshotActionsProvider { + data class ScreenshotAction( + val icon: Drawable?, + val text: String?, + val overrideTransition: Boolean, + val retrieveIntent: (Uri) -> Intent + ) + + fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent + fun getActions(context: Context, user: UserHandle): List<ScreenshotAction> +} + +class DefaultScreenshotActionsProvider @Inject constructor() : ScreenshotActionsProvider { + override fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent { + return ActionIntentCreator.createEdit(uri, context) + } + + override fun getActions( + context: Context, + user: UserHandle + ): List<ScreenshotActionsProvider.ScreenshotAction> { + val editAction = + ScreenshotActionsProvider.ScreenshotAction( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit), + context.resources.getString(R.string.screenshot_edit_label), + true + ) { uri -> + ActionIntentCreator.createEdit(uri, context) + } + val shareAction = + ScreenshotActionsProvider.ScreenshotAction( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share), + context.resources.getString(R.string.screenshot_share_label), + false + ) { uri -> + ActionIntentCreator.createShare(uri) + } + return listOf(editAction, shareAction) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index ee3e7ba9e8b3..c8e13bb8c2fc 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -21,7 +21,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; -import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS; import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; import static com.android.systemui.screenshot.LogConfig.DEBUG_UI; import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW; @@ -64,8 +63,6 @@ import android.util.Pair; import android.view.Display; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; -import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.ScrollCaptureResponse; @@ -77,8 +74,6 @@ import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import android.widget.Toast; -import android.window.OnBackInvokedCallback; -import android.window.OnBackInvokedDispatcher; import android.window.WindowContext; import com.android.internal.app.ChooserActivity; @@ -92,6 +87,10 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.res.R; import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback; +import com.android.systemui.screenshot.scroll.LongScreenshotActivity; +import com.android.systemui.screenshot.scroll.LongScreenshotData; +import com.android.systemui.screenshot.scroll.ScrollCaptureClient; +import com.android.systemui.screenshot.scroll.ScrollCaptureController; import com.android.systemui.util.Assert; import com.google.common.util.concurrent.ListenableFuture; @@ -165,7 +164,7 @@ public class ScreenshotController { /** * Structure returned by the SaveImageInBackgroundTask */ - static class SavedImageData { + public static class SavedImageData { public Uri uri; public List<Notification.Action> smartActions; public Notification.Action quickShareAction; @@ -205,7 +204,7 @@ public class ScreenshotController { void onActionsReady(ScreenshotController.QuickShareData quickShareData); } - interface TransitionDestination { + public interface TransitionDestination { /** * Allows the long screenshot activity to call back with a destination location (the bounds * on screen of the destination for the transitioning view) and a Runnable to be run once @@ -233,10 +232,11 @@ public class ScreenshotController { // From WizardManagerHelper.java private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; - private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; + static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; private final WindowContext mContext; private final FeatureFlags mFlags; + private final ScreenshotViewProxy mViewProxy; private final ScreenshotNotificationsController mNotificationsController; private final ScreenshotSmartActions mScreenshotSmartActions; private final UiEventLogger mUiEventLogger; @@ -265,14 +265,6 @@ public class ScreenshotController { private final UserManager mUserManager; private final AssistContentRequester mAssistContentRequester; - private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { - if (DEBUG_INPUT) { - Log.d(TAG, "Predictive Back callback dispatched"); - } - respondToKeyDismissal(); - }; - - private ScreenshotView mScreenshotView; private final MessageContainerController mMessageContainerController; private Bitmap mScreenBitmap; private SaveImageInBackgroundTask mSaveInBgTask; @@ -305,6 +297,7 @@ public class ScreenshotController { ScreenshotController( Context context, FeatureFlags flags, + ScreenshotViewProxy.Factory viewProxyFactory, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory, ScrollCaptureClient scrollCaptureClient, @@ -342,12 +335,7 @@ public class ScreenshotController { mScreenshotHandler = timeoutHandler; mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS); - mScreenshotHandler.setOnTimeoutRunnable(() -> { - if (DEBUG_UI) { - Log.d(TAG, "Corner timeout hit"); - } - dismissScreenshot(SCREENSHOT_INTERACTION_TIMEOUT); - }); + mDisplayId = displayId; mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class)); @@ -360,6 +348,15 @@ public class ScreenshotController { mMessageContainerController = messageContainerController; mAssistContentRequester = assistContentRequester; + mViewProxy = viewProxyFactory.getProxy(mContext, mDisplayId); + + mScreenshotHandler.setOnTimeoutRunnable(() -> { + if (DEBUG_UI) { + Log.d(TAG, "Corner timeout hit"); + } + mViewProxy.requestDismissal(SCREENSHOT_INTERACTION_TIMEOUT); + }); + mAccessibilityManager = AccessibilityManager.getInstance(mContext); // Setup the window that we are going to use @@ -383,7 +380,7 @@ public class ScreenshotController { @Override public void onReceive(Context context, Intent intent) { if (ClipboardOverlayController.COPY_OVERLAY_ACTION.equals(intent.getAction())) { - dismissScreenshot(SCREENSHOT_DISMISSED_OTHER); + mViewProxy.requestDismissal(SCREENSHOT_DISMISSED_OTHER); } } }; @@ -461,13 +458,13 @@ public class ScreenshotController { // The window is focusable by default setWindowFocusable(true); - mScreenshotView.requestFocus(); + mViewProxy.requestFocus(); enqueueScrollCaptureRequest(screenshot.getUserHandle()); attachWindow(); - boolean showFlash = true; + boolean showFlash; if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) { if (screenshot.getScreenBounds() != null && aspectRatiosMatch(screenshot.getBitmap(), screenshot.getInsets(), @@ -479,16 +476,15 @@ public class ScreenshotController { screenshot.setScreenBounds(new Rect(0, 0, screenshot.getBitmap().getWidth(), screenshot.getBitmap().getHeight())); } + } else { + showFlash = true; } - prepareAnimation(screenshot.getScreenBounds(), showFlash, () -> { - mMessageContainerController.onScreenshotTaken(screenshot); - }); + mViewProxy.prepareEntranceAnimation( + () -> startAnimation(screenshot.getScreenBounds(), showFlash, + () -> mMessageContainerController.onScreenshotTaken(screenshot))); - mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon( - mContext.getDrawable(R.drawable.overlay_badge_background), - screenshot.getUserHandle())); - mScreenshotView.setScreenshot(screenshot); + mViewProxy.setScreenshot(screenshot); // ignore system bar insets for the purpose of window layout mWindow.getDecorView().setOnApplyWindowInsetsListener( @@ -503,55 +499,44 @@ public class ScreenshotController { void prepareViewForNewScreenshot(ScreenshotData screenshot, String oldPackageName) { withWindowAttached(() -> { if (mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) { - mScreenshotView.announceForAccessibility(mContext.getResources().getString( + mViewProxy.announceForAccessibility(mContext.getResources().getString( R.string.screenshot_saving_work_profile_title)); } else { - mScreenshotView.announceForAccessibility( + mViewProxy.announceForAccessibility( mContext.getResources().getString(R.string.screenshot_saving_title)); } }); - mScreenshotView.reset(); + mViewProxy.reset(); - if (mScreenshotView.isAttachedToWindow()) { + if (mViewProxy.isAttachedToWindow()) { // if we didn't already dismiss for another reason - if (!mScreenshotView.isDismissing()) { + if (!mViewProxy.isDismissing()) { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0, oldPackageName); } if (DEBUG_WINDOW) { Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. " - + "(dismissing=" + mScreenshotView.isDismissing() + ")"); + + "(dismissing=" + mViewProxy.isDismissing() + ")"); } } - mScreenshotView.setPackageName(mPackageName); + mViewProxy.setPackageName(mPackageName); - mScreenshotView.updateOrientation( + mViewProxy.updateOrientation( mWindowManager.getCurrentWindowMetrics().getWindowInsets()); } /** - * Clears current screenshot + * Requests the view to dismiss the current screenshot (may be ignored, if screenshot is already + * being dismissed) */ - void dismissScreenshot(ScreenshotEvent event) { - if (DEBUG_DISMISS) { - Log.d(TAG, "dismissScreenshot"); - } - // If we're already animating out, don't restart the animation - if (mScreenshotView.isDismissing()) { - if (DEBUG_DISMISS) { - Log.v(TAG, "Already dismissing, ignoring duplicate command"); - } - return; - } - mUiEventLogger.log(event, 0, mPackageName); - mScreenshotHandler.cancelTimeout(); - mScreenshotView.animateDismissal(); + void requestDismissal(ScreenshotEvent event) { + mViewProxy.requestDismissal(event); } boolean isPendingSharedTransition() { - return mScreenshotView.isPendingSharedTransition(); + return mViewProxy.isPendingSharedTransition(); } // Any cleanup needed when the service is being destroyed. @@ -576,11 +561,7 @@ public class ScreenshotController { private void releaseMediaPlayer() { if (mScreenshotSoundController == null) return; - mScreenshotSoundController.releaseScreenshotSound(); - } - - private void respondToKeyDismissal() { - dismissScreenshot(SCREENSHOT_DISMISSED_OTHER); + mScreenshotSoundController.releaseScreenshotSoundAsync(); } /** @@ -591,31 +572,8 @@ public class ScreenshotController { Log.d(TAG, "reloadAssets()"); } - // Inflate the screenshot layout - mScreenshotView = (ScreenshotView) - LayoutInflater.from(mContext).inflate(R.layout.screenshot, null); - mMessageContainerController.setView(mScreenshotView); - mScreenshotView.addOnAttachStateChangeListener( - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(@NonNull View v) { - if (DEBUG_INPUT) { - Log.d(TAG, "Registering Predictive Back callback"); - } - mScreenshotView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback( - OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback); - } - - @Override - public void onViewDetachedFromWindow(@NonNull View v) { - if (DEBUG_INPUT) { - Log.d(TAG, "Unregistering Predictive Back callback"); - } - mScreenshotView.findOnBackInvokedDispatcher() - .unregisterOnBackInvokedCallback(mOnBackInvokedCallback); - } - }); - mScreenshotView.init(mUiEventLogger, new ScreenshotView.ScreenshotViewCallback() { + mMessageContainerController.setView(mViewProxy.getView()); + mViewProxy.setCallbacks(new ScreenshotView.ScreenshotViewCallback() { @Override public void onUserInteraction() { if (DEBUG_INPUT) { @@ -640,45 +598,12 @@ public class ScreenshotController { // TODO(159460485): Remove this when focus is handled properly in the system setWindowFocusable(false); } - }, mFlags); - mScreenshotView.setDefaultDisplay(mDisplayId); - mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis()); - - mScreenshotView.setOnKeyListener((v, keyCode, event) -> { - if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { - if (DEBUG_INPUT) { - Log.d(TAG, "onKeyEvent: " + keyCode); - } - respondToKeyDismissal(); - return true; - } - return false; }); if (DEBUG_WINDOW) { - Log.d(TAG, "adding OnComputeInternalInsetsListener"); + Log.d(TAG, "setContentView: " + mViewProxy.getView()); } - mScreenshotView.getViewTreeObserver().addOnComputeInternalInsetsListener(mScreenshotView); - if (DEBUG_WINDOW) { - Log.d(TAG, "setContentView: " + mScreenshotView); - } - setContentView(mScreenshotView); - } - - private void prepareAnimation(Rect screenRect, boolean showFlash, - Runnable onAnimationComplete) { - mScreenshotView.getViewTreeObserver().addOnPreDrawListener( - new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - if (DEBUG_WINDOW) { - Log.d(TAG, "onPreDraw: startAnimation"); - } - mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this); - startAnimation(screenRect, showFlash, onAnimationComplete); - return true; - } - }); + setContentView(mViewProxy.getView()); } private void enqueueScrollCaptureRequest(UserHandle owner) { @@ -694,13 +619,13 @@ public class ScreenshotController { if (mConfigChanges.applyNewConfig(mContext.getResources())) { // Hide the scroll chip until we know it's available in this // orientation - mScreenshotView.hideScrollChip(); + mViewProxy.hideScrollChip(); // Delay scroll capture eval a bit to allow the underlying activity // to set up in the new orientation. mScreenshotHandler.postDelayed(() -> { requestScrollCapture(owner); }, 150); - mScreenshotView.updateInsets( + mViewProxy.updateInsets( mWindowManager.getCurrentWindowMetrics().getWindowInsets()); // Screenshot animation calculations won't be valid anymore, // so just end @@ -759,16 +684,20 @@ public class ScreenshotController { + mLastScrollCaptureResponse.getWindowTitle() + "]"); final ScrollCaptureResponse response = mLastScrollCaptureResponse; - mScreenshotView.showScrollChip(response.getPackageName(), /* onClick */ () -> { + mViewProxy.showScrollChip(response.getPackageName(), /* onClick */ () -> { DisplayMetrics displayMetrics = new DisplayMetrics(); getDisplay().getRealMetrics(displayMetrics); Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId, new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)); - mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot, - mScreenshotTakenInPortrait); - // delay starting scroll capture to make sure the scrim is up before the app moves - mScreenshotView.post(() -> runBatchScrollCapture(response, owner)); + if (newScreenshot != null) { + // delay starting scroll capture to make sure scrim is up before the app moves + mViewProxy.prepareScrollingTransition( + response, mScreenBitmap, newScreenshot, mScreenshotTakenInPortrait, + () -> runBatchScrollCapture(response, owner)); + } else { + Log.wtf(TAG, "failed to capture current screenshot for scroll transition"); + } }); } catch (InterruptedException | ExecutionException e) { Log.e(TAG, "requestScrollCapture failed", e); @@ -794,19 +723,19 @@ public class ScreenshotController { return; } catch (InterruptedException | ExecutionException e) { Log.e(TAG, "Exception", e); - mScreenshotView.restoreNonScrollingUi(); + mViewProxy.restoreNonScrollingUi(); return; } if (longScreenshot.getHeight() == 0) { - mScreenshotView.restoreNonScrollingUi(); + mViewProxy.restoreNonScrollingUi(); return; } mLongScreenshotHolder.setLongScreenshot(longScreenshot); mLongScreenshotHolder.setTransitionDestinationCallback( (transitionDestination, onTransitionEnd) -> { - mScreenshotView.startLongScreenshotTransition( + mViewProxy.startLongScreenshotTransition( transitionDestination, onTransitionEnd, longScreenshot); // TODO: Do this via ActionIntentExecutor instead. @@ -882,16 +811,14 @@ public class ScreenshotController { } mWindowManager.removeViewImmediate(decorView); } - // Ensure that we remove the input monitor - if (mScreenshotView != null) { - mScreenshotView.stopInputListening(); - } + + mViewProxy.stopInputListening(); } private void playCameraSoundIfNeeded() { if (mScreenshotSoundController == null) return; // the controller is not-null only on the default display controller - mScreenshotSoundController.playCameraSound(); + mScreenshotSoundController.playScreenshotSoundAsync(); } /** @@ -932,7 +859,7 @@ public class ScreenshotController { } mScreenshotAnimation = - mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash); + mViewProxy.createScreenshotDropInAnimation(screenRect, showFlash); if (onAnimationComplete != null) { mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { @Override @@ -975,7 +902,7 @@ public class ScreenshotController { }; Pair<ActivityOptions, ExitTransitionCoordinator> transition = ActivityOptions.startSharedElementAnimation(mWindow, callbacks, null, - Pair.create(mScreenshotView.getScreenshotPreview(), + Pair.create(mViewProxy.getScreenshotPreview(), ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)); return transition; @@ -999,7 +926,7 @@ public class ScreenshotController { mCurrentRequestCallback.onFinish(); mCurrentRequestCallback = null; } - mScreenshotView.reset(); + mViewProxy.reset(); removeWindow(); mScreenshotHandler.cancelTimeout(); } @@ -1067,7 +994,7 @@ public class ScreenshotController { } private void doPostAnimation(ScreenshotController.SavedImageData imageData) { - mScreenshotView.setChipIntents(imageData); + mViewProxy.setChipIntents(imageData); } /** @@ -1084,11 +1011,11 @@ public class ScreenshotController { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); - mScreenshotView.addQuickShareChip(quickShareData.quickShareAction); + mViewProxy.addQuickShareChip(quickShareData.quickShareAction); } }); } else { - mScreenshotView.addQuickShareChip(quickShareData.quickShareAction); + mViewProxy.addQuickShareChip(quickShareData.quickShareAction); } }); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt new file mode 100644 index 000000000000..9354fd27ce5a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.ScrollCaptureResponse +import android.view.View +import android.view.ViewTreeObserver +import android.view.WindowInsets +import android.window.OnBackInvokedCallback +import android.window.OnBackInvokedDispatcher +import com.android.internal.logging.UiEventLogger +import com.android.systemui.log.DebugLogger.debugLog +import com.android.systemui.res.R +import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS +import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS +import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT +import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW +import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER +import com.android.systemui.screenshot.scroll.ScrollCaptureController +import com.android.systemui.screenshot.ui.ScreenshotAnimationController +import com.android.systemui.screenshot.ui.ScreenshotShelfView +import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Controls the screenshot view and viewModel. */ +class ScreenshotShelfViewProxy +@AssistedInject +constructor( + private val logger: UiEventLogger, + private val viewModel: ScreenshotViewModel, + private val staticActionsProvider: ScreenshotActionsProvider, + @Assisted private val context: Context, + @Assisted private val displayId: Int +) : ScreenshotViewProxy { + override val view: ScreenshotShelfView = + LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView + override val screenshotPreview: View + override var packageName: String = "" + override var callbacks: ScreenshotView.ScreenshotViewCallback? = null + override var screenshot: ScreenshotData? = null + set(value) { + viewModel.setScreenshotBitmap(value?.bitmap) + field = value + } + + override val isAttachedToWindow + get() = view.isAttachedToWindow + override var isDismissing = false + override var isPendingSharedTransition = false + + private val animationController = ScreenshotAnimationController(view) + + init { + ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context)) + addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" } + screenshotPreview = view.screenshotPreview + } + + override fun reset() { + animationController.cancel() + isPendingSharedTransition = false + viewModel.setScreenshotBitmap(null) + viewModel.setActions(listOf()) + } + override fun updateInsets(insets: WindowInsets) {} + override fun updateOrientation(insets: WindowInsets) {} + + override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator { + return animationController.getEntranceAnimation() + } + + override fun addQuickShareChip(quickShareAction: Notification.Action) {} + + override fun setChipIntents(imageData: ScreenshotController.SavedImageData) { + val staticActions = + staticActionsProvider.getActions(context, imageData.owner).map { + ActionButtonViewModel(it.icon, it.text) { + val intent = it.retrieveIntent(imageData.uri) + debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" } + isPendingSharedTransition = true + callbacks?.onAction(intent, imageData.owner, it.overrideTransition) + } + } + + viewModel.setActions(staticActions) + } + + override fun requestDismissal(event: ScreenshotEvent) { + debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" } + + // If we're already animating out, don't restart the animation + if (isDismissing) { + debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" } + return + } + logger.log(event, 0, packageName) + val animator = animationController.getExitAnimation() + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + isDismissing = true + } + override fun onAnimationEnd(animator: Animator) { + isDismissing = false + callbacks?.onDismiss() + } + } + ) + animator.start() + } + + override fun showScrollChip(packageName: String, onClick: Runnable) {} + + override fun hideScrollChip() {} + + override fun prepareScrollingTransition( + response: ScrollCaptureResponse, + screenBitmap: Bitmap, + newScreenshot: Bitmap, + screenshotTakenInPortrait: Boolean, + onTransitionPrepared: Runnable, + ) {} + + override fun startLongScreenshotTransition( + transitionDestination: Rect, + onTransitionEnd: Runnable, + longScreenshot: ScrollCaptureController.LongScreenshot + ) {} + + override fun restoreNonScrollingUi() {} + + override fun stopInputListening() {} + + override fun requestFocus() { + view.requestFocus() + } + + override fun announceForAccessibility(string: String) = view.announceForAccessibility(string) + + override fun prepareEntranceAnimation(runnable: Runnable) { + view.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + debugLog(DEBUG_WINDOW) { "onPreDraw: startAnimation" } + view.viewTreeObserver.removeOnPreDrawListener(this) + runnable.run() + return true + } + } + ) + } + + private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + val onBackInvokedCallback = OnBackInvokedCallback { + debugLog(DEBUG_INPUT) { "Predictive Back callback dispatched" } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + } + view.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + debugLog(DEBUG_INPUT) { "Registering Predictive Back callback" } + view + .findOnBackInvokedDispatcher() + ?.registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + onBackInvokedCallback + ) + } + + override fun onViewDetachedFromWindow(view: View) { + debugLog(DEBUG_INPUT) { "Unregistering Predictive Back callback" } + view + .findOnBackInvokedDispatcher() + ?.unregisterOnBackInvokedCallback(onBackInvokedCallback) + } + } + ) + } + private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + view.setOnKeyListener( + object : View.OnKeyListener { + override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { + debugLog(DEBUG_INPUT) { "onKeyEvent: $keyCode" } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + return true + } + return false + } + } + ) + } + + @AssistedFactory + interface Factory : ScreenshotViewProxy.Factory { + override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt index 2c0bddecc58e..d3a7fc4a3e4a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSoundController.kt @@ -21,22 +21,34 @@ import android.util.Log import com.android.app.tracing.coroutines.async import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.google.errorprone.annotations.CanIgnoreReturnValue import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout /** Controls sound reproduction after a screenshot is taken. */ interface ScreenshotSoundController { /** Reproduces the camera sound. */ - @CanIgnoreReturnValue fun playCameraSound(): Deferred<Unit> + suspend fun playScreenshotSound() - /** Releases the sound. [playCameraSound] behaviour is undefined after this has been called. */ - @CanIgnoreReturnValue fun releaseScreenshotSound(): Deferred<Unit> + /** + * Releases the sound. [playScreenshotSound] behaviour is undefined after this has been called. + */ + suspend fun releaseScreenshotSound() + + /** Reproduces the camera sound. Used for compatibility with Java code. */ + fun playScreenshotSoundAsync() + + /** + * Releases the sound. [playScreenshotSound] behaviour is undefined after this has been called. + * Used for compatibility with Java code. + */ + fun releaseScreenshotSoundAsync() } class ScreenshotSoundControllerImpl @@ -47,8 +59,8 @@ constructor( @Background private val bgDispatcher: CoroutineDispatcher ) : ScreenshotSoundController { - val player: Deferred<MediaPlayer?> = - coroutineScope.async("loadCameraSound", bgDispatcher) { + private val player: Deferred<MediaPlayer?> = + coroutineScope.async("loadScreenshotSound", bgDispatcher) { try { soundProvider.getScreenshotSound() } catch (e: IllegalStateException) { @@ -57,8 +69,8 @@ constructor( } } - override fun playCameraSound(): Deferred<Unit> { - return coroutineScope.async("playCameraSound", bgDispatcher) { + override suspend fun playScreenshotSound() { + withContext(bgDispatcher) { try { player.await()?.start() } catch (e: IllegalStateException) { @@ -68,8 +80,8 @@ constructor( } } - override fun releaseScreenshotSound(): Deferred<Unit> { - return coroutineScope.async("releaseScreenshotSound", bgDispatcher) { + override suspend fun releaseScreenshotSound() { + withContext(bgDispatcher) { try { withTimeout(1.seconds) { player.await()?.release() } } catch (e: TimeoutCancellationException) { @@ -79,6 +91,14 @@ constructor( } } + override fun playScreenshotSoundAsync() { + coroutineScope.launch { playScreenshotSound() } + } + + override fun releaseScreenshotSoundAsync() { + coroutineScope.launch { releaseScreenshotSound() } + } + private companion object { const val TAG = "ScreenshotSoundControllerImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index be30a1576b21..cb2dba00890b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -26,6 +26,7 @@ import static com.android.systemui.screenshot.LogConfig.DEBUG_SCROLL; import static com.android.systemui.screenshot.LogConfig.DEBUG_UI; import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW; import static com.android.systemui.screenshot.LogConfig.logTag; +import static com.android.systemui.screenshot.ScreenshotController.SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS; import static java.util.Objects.requireNonNull; @@ -33,6 +34,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.BroadcastOptions; import android.app.Notification; @@ -90,6 +92,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.res.R; +import com.android.systemui.screenshot.scroll.ScrollCaptureController; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.shared.system.InputMonitorCompat; import com.android.systemui.shared.system.QuickStepContract; @@ -102,7 +105,7 @@ import java.util.ArrayList; public class ScreenshotView extends FrameLayout implements ViewTreeObserver.OnComputeInternalInsetsListener { - interface ScreenshotViewCallback { + public interface ScreenshotViewCallback { void onUserInteraction(); void onAction(Intent intent, UserHandle owner, boolean overrideTransition); @@ -168,7 +171,6 @@ public class ScreenshotView extends FrameLayout implements private ScreenshotData mScreenshotData; private final InteractionJankMonitor mInteractionJankMonitor; - private long mDefaultTimeoutOfTimeoutHandler; private FeatureFlags mFlags; private final Bundle mInteractiveBroadcastOption; @@ -244,10 +246,6 @@ public class ScreenshotView extends FrameLayout implements return InteractionJankMonitor.getInstance(); } - void setDefaultTimeoutMillis(long timeout) { - mDefaultTimeoutOfTimeoutHandler = timeout; - } - public void hideScrollChip() { mScrollChip.setVisibility(View.GONE); } @@ -426,15 +424,15 @@ public class ScreenshotView extends FrameLayout implements return mScreenshotPreview; } - /** - * Set up the logger and callback on dismissal. - * - * Note: must be called before any other (non-constructor) method or null pointer exceptions - * may occur. - */ - void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks, FeatureFlags flags) { + void setUiEventLogger(UiEventLogger uiEventLogger) { mUiEventLogger = uiEventLogger; + } + + void setCallbacks(ScreenshotViewCallback callbacks) { mCallbacks = callbacks; + } + + void setFlags(FeatureFlags flags) { mFlags = flags; } @@ -755,7 +753,7 @@ public class ScreenshotView extends FrameLayout implements InteractionJankMonitor.Configuration.Builder.withView( CUJ_TAKE_SCREENSHOT, mScreenshotStatic) .setTag("Actions") - .setTimeout(mDefaultTimeoutOfTimeoutHandler); + .setTimeout(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS); mInteractionJankMonitor.begin(builder); } }); @@ -781,7 +779,7 @@ public class ScreenshotView extends FrameLayout implements return animator; } - void badgeScreenshot(Drawable badge) { + void badgeScreenshot(@Nullable Drawable badge) { mScreenshotBadge.setImageDrawable(badge); mScreenshotBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt new file mode 100644 index 000000000000..6be32a97f4e2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.animation.Animator +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.view.ScrollCaptureResponse +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets +import com.android.systemui.screenshot.scroll.ScrollCaptureController + +/** Abstraction of the surface between ScreenshotController and ScreenshotView */ +interface ScreenshotViewProxy { + val view: ViewGroup + val screenshotPreview: View + + var packageName: String + var callbacks: ScreenshotView.ScreenshotViewCallback? + var screenshot: ScreenshotData? + + val isAttachedToWindow: Boolean + val isDismissing: Boolean + val isPendingSharedTransition: Boolean + + fun reset() + fun updateInsets(insets: WindowInsets) + fun updateOrientation(insets: WindowInsets) + fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator + fun addQuickShareChip(quickShareAction: Notification.Action) + fun setChipIntents(imageData: ScreenshotController.SavedImageData) + fun requestDismissal(event: ScreenshotEvent) + + fun showScrollChip(packageName: String, onClick: Runnable) + fun hideScrollChip() + fun prepareScrollingTransition( + response: ScrollCaptureResponse, + screenBitmap: Bitmap, + newScreenshot: Bitmap, + screenshotTakenInPortrait: Boolean, + onTransitionPrepared: Runnable, + ) + fun startLongScreenshotTransition( + transitionDestination: Rect, + onTransitionEnd: Runnable, + longScreenshot: ScrollCaptureController.LongScreenshot + ) + fun restoreNonScrollingUi() + + fun stopInputListening() + fun requestFocus() + fun announceForAccessibility(string: String) + fun prepareEntranceAnimation(runnable: Runnable) + + interface Factory { + fun getProxy(context: Context, displayId: Int): ScreenshotViewProxy + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt index e464fd0c8e8e..bc3375502dd4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt @@ -136,7 +136,7 @@ constructor( fun onCloseSystemDialogsReceived() { screenshotControllers.forEach { (_, screenshotController) -> if (!screenshotController.isPendingSharedTransition) { - screenshotController.dismissScreenshot(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER) + screenshotController.requestDismissal(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER) } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index 0991c9a326c8..9cf347bc8569 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -53,9 +53,9 @@ import android.widget.Toast; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.internal.util.ScreenshotRequest; -import com.android.systemui.res.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.res.R; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -89,7 +89,7 @@ public class TakeScreenshotService extends Service { // TODO(b/295143676): move receiver inside executor when the flag is enabled. mTakeScreenshotExecutor.get().onCloseSystemDialogsReceived(); } else if (!mScreenshot.isPendingSharedTransition()) { - mScreenshot.dismissScreenshot(SCREENSHOT_DISMISSED_OTHER); + mScreenshot.requestDismissal(SCREENSHOT_DISMISSED_OTHER); } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java b/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java index 71c2cb4a5cb9..5df6c45295b6 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java @@ -40,7 +40,7 @@ public class TimeoutHandler extends Handler { private final Context mContext; private Runnable mOnTimeout; - private int mDefaultTimeout = DEFAULT_TIMEOUT_MILLIS; + int mDefaultTimeout = DEFAULT_TIMEOUT_MILLIS; @Inject public TimeoutHandler(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java index aa23d6bed5e1..d87d85b93b9d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java @@ -52,7 +52,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLogger.UiEventEnum; import com.android.settingslib.Utils; import com.android.systemui.res.R; -import com.android.systemui.screenshot.CropView; +import com.android.systemui.screenshot.scroll.CropView; import com.android.systemui.settings.UserTracker; import javax.inject.Inject; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java index 3797b8b41e5a..9118ee1dfc73 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java @@ -16,25 +16,36 @@ package com.android.systemui.screenshot.dagger; +import static com.android.systemui.Flags.screenshotShelfUi; + import android.app.Service; +import android.view.accessibility.AccessibilityManager; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.screenshot.DefaultScreenshotActionsProvider; import com.android.systemui.screenshot.ImageCapture; import com.android.systemui.screenshot.ImageCaptureImpl; +import com.android.systemui.screenshot.LegacyScreenshotViewProxy; import com.android.systemui.screenshot.RequestProcessor; +import com.android.systemui.screenshot.ScreenshotActionsProvider; import com.android.systemui.screenshot.ScreenshotPolicy; import com.android.systemui.screenshot.ScreenshotPolicyImpl; import com.android.systemui.screenshot.ScreenshotProxyService; import com.android.systemui.screenshot.ScreenshotRequestProcessor; +import com.android.systemui.screenshot.ScreenshotShelfViewProxy; import com.android.systemui.screenshot.ScreenshotSoundController; import com.android.systemui.screenshot.ScreenshotSoundControllerImpl; import com.android.systemui.screenshot.ScreenshotSoundProvider; import com.android.systemui.screenshot.ScreenshotSoundProviderImpl; +import com.android.systemui.screenshot.ScreenshotViewProxy; import com.android.systemui.screenshot.TakeScreenshotService; import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService; import com.android.systemui.screenshot.appclips.AppClipsService; +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel; import dagger.Binds; import dagger.Module; +import dagger.Provides; import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; @@ -81,4 +92,26 @@ public abstract class ScreenshotModule { @Binds abstract ScreenshotSoundController bindScreenshotSoundController( ScreenshotSoundControllerImpl screenshotSoundProviderImpl); + + @Binds + abstract ScreenshotActionsProvider bindScreenshotActionsProvider( + DefaultScreenshotActionsProvider defaultScreenshotActionsProvider); + + @Provides + @SysUISingleton + static ScreenshotViewModel providesScreenshotViewModel( + AccessibilityManager accessibilityManager) { + return new ScreenshotViewModel(accessibilityManager); + } + + @Provides + static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory( + ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory, + LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) { + if (screenshotShelfUi()) { + return shelfScreenshotViewProxyFactory; + } else { + return legacyScreenshotViewProxyFactory; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/CropView.java index 2f411ea3cb33..5e561cfb14a1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/CropView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import android.animation.ValueAnimator; import android.content.Context; @@ -265,7 +265,7 @@ public class CropView extends View { Log.w(TAG, "No boundary selected"); break; } - Log.i(TAG, "Updated mCrop: " + mCrop); + Log.i(TAG, "Updated mCrop: " + mCrop); invalidate(); } @@ -385,7 +385,7 @@ public class CropView extends View { /** * @param action either ACTION_DOWN, ACTION_UP or ACTION_MOVE. - * @param x coordinate of the relevant pointer. + * @param x x-coordinate of the relevant pointer. */ private void updateListener(int action, float x) { if (mCropInteractionListener != null && isVertical(mCurrentDraggingBoundary)) { @@ -643,11 +643,13 @@ public class CropView extends View { /** * Listen for crop motion events and state. */ - public interface CropInteractionListener { + interface CropInteractionListener { void onCropDragStarted(CropBoundary boundary, float boundaryPosition, int boundaryPositionPx, float horizontalCenter, float x); + void onCropDragMoved(CropBoundary boundary, float boundaryPosition, int boundaryPositionPx, float horizontalCenter, float x); + void onCropDragComplete(); } @@ -675,8 +677,7 @@ public class CropView extends View { out.writeParcelable(mCrop, 0); } - public static final Parcelable.Creator<SavedState> CREATOR - = new Parcelable.Creator<SavedState>() { + public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageLoader.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ImageLoader.java index 7ee7c319799c..df86d69b2527 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageLoader.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ImageLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import android.annotation.Nullable; import android.content.ContentResolver; @@ -35,20 +35,20 @@ import java.io.InputStream; import javax.inject.Inject; /** Loads images. */ -public class ImageLoader { +class ImageLoader { private final ContentResolver mResolver; static class Result { - @Nullable Uri uri; - @Nullable File fileName; - @Nullable Bitmap bitmap; + @Nullable Uri mUri; + @Nullable File mFilename; + @Nullable Bitmap mBitmap; @Override public String toString() { final StringBuilder sb = new StringBuilder("Result{"); - sb.append("uri=").append(uri); - sb.append(", fileName=").append(fileName); - sb.append(", bitmap=").append(bitmap); + sb.append("uri=").append(mUri); + sb.append(", fileName=").append(mFilename); + sb.append(", bitmap=").append(mBitmap); sb.append('}'); return sb.toString(); } @@ -69,11 +69,10 @@ public class ImageLoader { return CallbackToFutureAdapter.getFuture(completer -> { Result result = new Result(); try (InputStream in = mResolver.openInputStream(uri)) { - result.uri = uri; - result.bitmap = BitmapFactory.decodeStream(in); + result.mUri = uri; + result.mBitmap = BitmapFactory.decodeStream(in); completer.set(result); - } - catch (IOException e) { + } catch (IOException e) { completer.setException(e); } return "BitmapFactory#decodeStream"; @@ -91,8 +90,8 @@ public class ImageLoader { return CallbackToFutureAdapter.getFuture(completer -> { try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { Result result = new Result(); - result.fileName = file; - result.bitmap = BitmapFactory.decodeStream(in); + result.mFilename = file; + result.mBitmap = BitmapFactory.decodeStream(in); completer.set(result); } catch (IOException e) { completer.setException(e); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTile.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ImageTile.java index a95c91bfeceb..c9c297e53bd4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTile.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ImageTile.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import static android.graphics.ColorSpace.Named.SRGB; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ImageTileSet.java index 356f67e7ea00..76a72f7e4adf 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageTileSet.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ImageTileSet.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import android.annotation.AnyThread; import android.graphics.Bitmap; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java index 00d480a76355..1e1a577ebd4a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import android.app.Activity; import android.app.ActivityOptions; @@ -47,11 +47,14 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.view.OneShotPreDrawListener; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.res.R; -import com.android.systemui.screenshot.CropView.CropBoundary; -import com.android.systemui.screenshot.ScrollCaptureController.LongScreenshot; -import com.android.systemui.settings.UserTracker; +import com.android.systemui.screenshot.ActionIntentCreator; +import com.android.systemui.screenshot.ActionIntentExecutor; +import com.android.systemui.screenshot.ImageExporter; +import com.android.systemui.screenshot.LogConfig; +import com.android.systemui.screenshot.ScreenshotEvent; +import com.android.systemui.screenshot.scroll.CropView.CropBoundary; +import com.android.systemui.screenshot.scroll.ScrollCaptureController.LongScreenshot; import com.google.common.util.concurrent.ListenableFuture; @@ -81,8 +84,6 @@ public class LongScreenshotActivity extends Activity { private final ImageExporter mImageExporter; private final LongScreenshotData mLongScreenshotHolder; private final ActionIntentExecutor mActionExecutor; - private final FeatureFlags mFeatureFlags; - private final UserTracker mUserTracker; private ImageView mPreview; private ImageView mTransitionView; @@ -113,16 +114,13 @@ public class LongScreenshotActivity extends Activity { @Inject public LongScreenshotActivity(UiEventLogger uiEventLogger, ImageExporter imageExporter, @Main Executor mainExecutor, @Background Executor bgExecutor, - LongScreenshotData longScreenshotHolder, ActionIntentExecutor actionExecutor, - FeatureFlags featureFlags, UserTracker userTracker) { + LongScreenshotData longScreenshotHolder, ActionIntentExecutor actionExecutor) { mUiEventLogger = uiEventLogger; mUiExecutor = mainExecutor; mBackgroundExecutor = bgExecutor; mImageExporter = imageExporter; mLongScreenshotHolder = longScreenshotHolder; mActionExecutor = actionExecutor; - mFeatureFlags = featureFlags; - mUserTracker = userTracker; } @@ -265,13 +263,13 @@ public class LongScreenshotActivity extends Activity { private void onCachedImageLoaded(ImageLoader.Result imageResult) { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_ACTIVITY_CACHED_IMAGE_LOADED); - BitmapDrawable drawable = new BitmapDrawable(getResources(), imageResult.bitmap); + BitmapDrawable drawable = new BitmapDrawable(getResources(), imageResult.mBitmap); mPreview.setImageDrawable(drawable); mPreview.setAlpha(1f); - mMagnifierView.setDrawable(drawable, imageResult.bitmap.getWidth(), - imageResult.bitmap.getHeight()); + mMagnifierView.setDrawable(drawable, imageResult.mBitmap.getWidth(), + imageResult.mBitmap.getHeight()); mCropView.setVisibility(View.VISIBLE); - mSavedImagePath = imageResult.fileName; + mSavedImagePath = imageResult.mFilename; setButtonsEnabled(true); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotData.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotData.java index f549faf2414a..ebac5bf2debd 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotData.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/LongScreenshotData.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.screenshot.ScreenshotController; import java.util.concurrent.atomic.AtomicReference; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/MagnifierView.java index 0c543cd8909c..0a1a74735648 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/MagnifierView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/MagnifierView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -64,16 +64,16 @@ public class MagnifierView extends View implements CropView.CropInteractionListe private ViewPropertyAnimator mTranslationAnimator; private final Animator.AnimatorListener mTranslationAnimatorListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - mTranslationAnimator = null; - } - - @Override - public void onAnimationEnd(Animator animation) { - mTranslationAnimator = null; - } - }; + @Override + public void onAnimationCancel(Animator animation) { + mTranslationAnimator = null; + } + + @Override + public void onAnimationEnd(Animator animation) { + mTranslationAnimator = null; + } + }; public MagnifierView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureClient.java index e93f737308ba..0e4334314ec2 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureClient.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import static com.android.systemui.screenshot.LogConfig.DEBUG_SCROLL; @@ -46,6 +46,7 @@ import androidx.concurrent.futures.CallbackToFutureAdapter.Completer; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.screenshot.LogConfig; import com.google.common.util.concurrent.ListenableFuture; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java index bb34ede2cf5e..f4c77da674b0 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ScrollCaptureController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import android.content.Context; import android.graphics.Bitmap; @@ -30,8 +30,10 @@ import androidx.concurrent.futures.CallbackToFutureAdapter.Completer; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.screenshot.ScrollCaptureClient.CaptureResult; -import com.android.systemui.screenshot.ScrollCaptureClient.Session; +import com.android.systemui.screenshot.LogConfig; +import com.android.systemui.screenshot.ScreenshotEvent; +import com.android.systemui.screenshot.scroll.ScrollCaptureClient.CaptureResult; +import com.android.systemui.screenshot.scroll.ScrollCaptureClient.Session; import com.google.common.util.concurrent.ListenableFuture; @@ -75,7 +77,7 @@ public class ScrollCaptureController { private String mWindowOwner; private volatile boolean mCancelled; - static class LongScreenshot { + public static class LongScreenshot { private final ImageTileSet mImageTileSet; private final Session mSession; // TODO: Add UserHandle so LongScreenshots can adhere to work profile screenshot policy @@ -85,7 +87,7 @@ public class ScrollCaptureController { mImageTileSet = imageTileSet; } - /** Returns a bitmap containing the combinded result. */ + /** Returns a bitmap containing the combined result. */ public Bitmap toBitmap() { return mImageTileSet.toBitmap(); } @@ -167,7 +169,7 @@ public class ScrollCaptureController { * {@link ScrollCaptureResponse#isConnected() connected}. * @return a future ImageTile set containing the result */ - ListenableFuture<LongScreenshot> run(ScrollCaptureResponse response) { + public ListenableFuture<LongScreenshot> run(ScrollCaptureResponse response) { mCancelled = false; return CallbackToFutureAdapter.getFuture(completer -> { mCaptureCompleter = completer; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TiledImageDrawable.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/TiledImageDrawable.java index 71df369aa7b8..00455bcfbd99 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TiledImageDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/TiledImageDrawable.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import android.annotation.Nullable; import android.graphics.Canvas; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt new file mode 100644 index 000000000000..2c178736d9c4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.view.View + +class ScreenshotAnimationController(private val view: View) { + private var animator: Animator? = null + + fun getEntranceAnimation(): Animator { + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.addUpdateListener { view.alpha = it.animatedFraction } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + view.alpha = 0f + } + override fun onAnimationEnd(animator: Animator) { + view.alpha = 1f + } + } + ) + this.animator = animator + return animator + } + + fun getExitAnimation(): Animator { + val animator = ValueAnimator.ofFloat(1f, 0f) + animator.addUpdateListener { view.alpha = it.animatedValue as Float } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + view.alpha = 1f + } + override fun onAnimationEnd(animator: Animator) { + view.alpha = 0f + } + } + ) + this.animator = animator + return animator + } + + fun cancel() { + animator?.cancel() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt new file mode 100644 index 000000000000..747ad4f9e48c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.systemui.res.R + +class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) : + ConstraintLayout(context, attrs) { + lateinit var screenshotPreview: ImageView + + override fun onFinishInflate() { + super.onFinishInflate() + screenshotPreview = requireViewById(R.id.screenshot_preview) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt new file mode 100644 index 000000000000..a5825b5f7797 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.binder + +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.android.systemui.res.R +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel + +object ActionButtonViewBinder { + /** Binds the given view to the given view-model */ + fun bind(view: View, viewModel: ActionButtonViewModel) { + val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon) + val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text) + iconView.setImageDrawable(viewModel.icon) + textView.text = viewModel.name + setMargins(iconView, textView, viewModel.name?.isNotEmpty() ?: false) + if (viewModel.onClicked != null) { + view.setOnClickListener { viewModel.onClicked.invoke() } + } else { + view.setOnClickListener(null) + } + view.visibility = View.VISIBLE + view.alpha = 1f + } + + private fun setMargins(iconView: View, textView: View, hasText: Boolean) { + val iconParams = iconView.layoutParams as LinearLayout.LayoutParams + val textParams = textView.layoutParams as LinearLayout.LayoutParams + if (hasText) { + iconParams.marginStart = iconView.dpToPx(R.dimen.overlay_action_chip_padding_start) + iconParams.marginEnd = iconView.dpToPx(R.dimen.overlay_action_chip_spacing) + textParams.marginStart = 0 + textParams.marginEnd = textView.dpToPx(R.dimen.overlay_action_chip_padding_end) + } else { + val paddingHorizontal = + iconView.dpToPx(R.dimen.overlay_action_chip_icon_only_padding_horizontal) + iconParams.marginStart = paddingHorizontal + iconParams.marginEnd = paddingHorizontal + } + iconView.layoutParams = iconParams + textView.layoutParams = textParams + } + + private fun View.dpToPx(dimenId: Int): Int { + return this.resources.getDimensionPixelSize(dimenId) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt new file mode 100644 index 000000000000..3bcd52cbc99e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.binder + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel +import com.android.systemui.util.children +import kotlinx.coroutines.launch + +object ScreenshotShelfViewBinder { + fun bind( + view: ViewGroup, + viewModel: ScreenshotViewModel, + layoutInflater: LayoutInflater, + ) { + val previewView: ImageView = view.requireViewById(R.id.screenshot_preview) + val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border) + previewView.clipToOutline = true + val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions) + view.requireViewById<View>(R.id.screenshot_dismiss_button).visibility = + if (viewModel.showDismissButton) View.VISIBLE else View.GONE + + view.repeatWhenAttached { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.preview.collect { bitmap -> + if (bitmap != null) { + previewView.setImageBitmap(bitmap) + previewView.visibility = View.VISIBLE + previewBorder.visibility = View.VISIBLE + } else { + previewView.visibility = View.GONE + previewBorder.visibility = View.GONE + } + } + } + launch { + viewModel.actions.collect { actions -> + if (actions.isNotEmpty()) { + view + .requireViewById<View>(R.id.actions_container_background) + .visibility = View.VISIBLE + } + val viewPool = actionsContainer.children.toList() + actionsContainer.removeAllViews() + val actionButtons = + List(actions.size) { + viewPool.getOrElse(it) { + layoutInflater.inflate( + R.layout.overlay_action_chip, + actionsContainer, + false + ) + } + } + actionButtons.zip(actions).forEach { + actionsContainer.addView(it.first) + ActionButtonViewBinder.bind(it.first, it.second) + } + } + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt new file mode 100644 index 000000000000..6ee970534352 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.viewmodel + +import android.graphics.drawable.Drawable + +data class ActionButtonViewModel( + val icon: Drawable?, + val name: String?, + val onClicked: (() -> Unit)? +) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt new file mode 100644 index 000000000000..3a652d90bb78 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.viewmodel + +import android.graphics.Bitmap +import android.view.accessibility.AccessibilityManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) { + private val _preview = MutableStateFlow<Bitmap?>(null) + val preview: StateFlow<Bitmap?> = _preview + private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>()) + val actions: StateFlow<List<ActionButtonViewModel>> = _actions + val showDismissButton: Boolean + get() = accessibilityManager.isEnabled + + fun setScreenshotBitmap(bitmap: Bitmap?) { + _preview.value = bitmap + } + + fun setActions(actions: List<ActionButtonViewModel>) { + _actions.value = actions + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index d1055c77ab8f..9cfedaab89f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -76,6 +76,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor; import com.android.systemui.keyguard.shared.model.DismissAction; import com.android.systemui.keyguard.shared.model.KeyguardDone; +import com.android.systemui.keyguard.shared.model.KeyguardState; +import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.TaskbarDelegate; @@ -101,6 +103,8 @@ import com.android.systemui.util.kotlin.JavaAdapter; import dagger.Lazy; +import kotlin.Unit; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashSet; @@ -112,6 +116,7 @@ import javax.inject.Inject; import kotlinx.coroutines.CoroutineDispatcher; import kotlinx.coroutines.ExperimentalCoroutinesApi; +import kotlinx.coroutines.Job; /** * Manages creating, showing, hiding and resetting the keyguard within the status bar. Calls back @@ -158,6 +163,9 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private final BouncerView mPrimaryBouncerView; private final Lazy<ShadeController> mShadeController; + private Job mListenForAlternateBouncerTransitionSteps = null; + private Job mListenForKeyguardAuthenticatedBiometricsHandled = null; + // Local cache of expansion events, to avoid duplicates private float mFraction = -1f; private boolean mTracking = false; @@ -482,6 +490,26 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mDockManager.addListener(mDockEventListener); mIsDocked = mDockManager.isDocked(); } + if (mListenForAlternateBouncerTransitionSteps != null) { + mListenForAlternateBouncerTransitionSteps.cancel(null); + } + mListenForAlternateBouncerTransitionSteps = null; + if (mListenForKeyguardAuthenticatedBiometricsHandled != null) { + mListenForKeyguardAuthenticatedBiometricsHandled.cancel(null); + } + mListenForKeyguardAuthenticatedBiometricsHandled = null; + if (!DeviceEntryUdfpsRefactor.isEnabled()) { + mListenForAlternateBouncerTransitionSteps = mJavaAdapter.alwaysCollectFlow( + mKeyguardTransitionInteractor.transitionStepsFromState( + KeyguardState.ALTERNATE_BOUNCER), + this::consumeFromAlternateBouncerTransitionSteps + ); + + mListenForKeyguardAuthenticatedBiometricsHandled = mJavaAdapter.alwaysCollectFlow( + mPrimaryBouncerInteractor.getKeyguardAuthenticatedBiometricsHandled(), + this::consumeKeyguardAuthenticatedBiometricsHandled + ); + } if (KeyguardWmStateRefactor.isEnabled()) { // Show the keyguard views whenever we've told WM that the lockscreen is visible. @@ -500,6 +528,22 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } } + @VisibleForTesting + void consumeFromAlternateBouncerTransitionSteps(TransitionStep step) { + hideAlternateBouncer(false); + } + + /** + * Required without fix for b/328643370: missing AlternateBouncer (when occluded) => Gone + * transition. + */ + @VisibleForTesting + void consumeKeyguardAuthenticatedBiometricsHandled(Unit handled) { + if (mAlternateBouncerInteractor.isVisibleState()) { + hideAlternateBouncer(false); + } + } + private void consumeShowStatusBarKeyguardView(boolean show) { if (show != mLastShowing) { if (show) { @@ -1441,7 +1485,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mPrimaryBouncerInteractor.notifyKeyguardAuthenticatedBiometrics(strongAuth); if (mAlternateBouncerInteractor.isVisibleState()) { - hideAlternateBouncer(false); executeAfterKeyguardGoneAction(); } } diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index 5882b56fff6a..572a6c12f3e1 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -113,7 +113,7 @@ android:excludeFromRecents="true" /> - <activity android:name="com.android.systemui.screenshot.ScrollViewActivity" + <activity android:name="com.android.systemui.screenshot.scroll.ScrollViewActivity" android:exported="false" /> <activity android:name="com.android.systemui.screenshot.RecyclerViewActivity" diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt index 87391cce9136..d410dac1b2e4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -36,6 +37,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.mockito.Mockito.verify @ExperimentalCoroutinesApi @RunWith(JUnit4::class) @@ -48,6 +50,20 @@ class AlternateBouncerViewModelTest : SysuiTestCase() { private val underTest = kosmos.alternateBouncerViewModel @Test + fun showPrimaryBouncer() = + testScope.runTest { + underTest.showPrimaryBouncer() + verify(statusBarKeyguardViewManager).showPrimaryBouncer(any()) + } + + @Test + fun hideAlternateBouncer() = + testScope.runTest { + underTest.hideAlternateBouncer() + verify(statusBarKeyguardViewManager).hideAlternateBouncer(any()) + } + + @Test fun transitionToAlternateBouncer_scrimAlphaUpdate() = testScope.runTest { val scrimAlphas by collectValues(underTest.scrimAlpha) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java deleted file mode 100644 index 9ea30d676dc5..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ActionProxyReceiverTest.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.screenshot; - -import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_SHARE; -import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; -import static com.android.systemui.screenshot.ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED; -import static com.android.systemui.statusbar.phone.CentralSurfaces.SYSTEM_DIALOG_REASON_SCREENSHOT; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.app.PendingIntent; -import android.content.Intent; -import android.os.Bundle; -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.settings.FakeDisplayTracker; -import com.android.systemui.shared.system.ActivityManagerWrapper; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.stubbing.Answer; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - -@RunWith(AndroidTestingRunner.class) -@SmallTest -public class ActionProxyReceiverTest extends SysuiTestCase { - @Mock - private ActivityManagerWrapper mMockActivityManagerWrapper; - @Mock - private ScreenshotSmartActions mMockScreenshotSmartActions; - @Mock - private PendingIntent mMockPendingIntent; - @Mock - private ActivityStarter mActivityStarter; - - private Intent mIntent; - private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); - - @Before - public void setup() throws InterruptedException, ExecutionException, TimeoutException { - MockitoAnnotations.initMocks(this); - mIntent = new Intent(mContext, ActionProxyReceiver.class) - .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, mMockPendingIntent); - } - - @Test - public void testPendingIntentSentWithStatusBar() throws PendingIntent.CanceledException { - ActionProxyReceiver actionProxyReceiver = constructActionProxyReceiver(); - // ensure that the pending intent call is passed through - doAnswer((Answer<Object>) invocation -> { - ((Runnable) invocation.getArgument(0)).run(); - return null; - }).when(mActivityStarter).executeRunnableDismissingKeyguard( - any(Runnable.class), isNull(), anyBoolean(), anyBoolean(), anyBoolean()); - - actionProxyReceiver.onReceive(mContext, mIntent); - - verify(mMockActivityManagerWrapper).closeSystemWindows(SYSTEM_DIALOG_REASON_SCREENSHOT); - verify(mActivityStarter).executeRunnableDismissingKeyguard( - any(Runnable.class), isNull(), eq(true), eq(true), eq(true)); - verify(mMockPendingIntent).send( - eq(mContext), anyInt(), isNull(), isNull(), isNull(), isNull(), any(Bundle.class)); - } - - @Test - public void testSmartActionsNotNotifiedByDefault() { - ActionProxyReceiver actionProxyReceiver = constructActionProxyReceiver(); - - actionProxyReceiver.onReceive(mContext, mIntent); - - verify(mMockScreenshotSmartActions, never()) - .notifyScreenshotAction(anyString(), anyString(), anyBoolean(), - any(Intent.class)); - } - - @Test - public void testSmartActionsNotifiedIfEnabled() { - ActionProxyReceiver actionProxyReceiver = constructActionProxyReceiver(); - mIntent.putExtra(EXTRA_SMART_ACTIONS_ENABLED, true); - String testId = "testID"; - mIntent.putExtra(EXTRA_ID, testId); - - actionProxyReceiver.onReceive(mContext, mIntent); - - verify(mMockScreenshotSmartActions).notifyScreenshotAction( - testId, ACTION_TYPE_SHARE, false, null); - } - - private ActionProxyReceiver constructActionProxyReceiver() { - return new ActionProxyReceiver( - mMockActivityManagerWrapper, - mMockScreenshotSmartActions, - mDisplayTracker, - mActivityStarter - ); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DeleteScreenshotReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DeleteScreenshotReceiverTest.java deleted file mode 100644 index d58f47a0469a..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DeleteScreenshotReceiverTest.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.screenshot; - -import static com.android.systemui.screenshot.ScreenshotController.ACTION_TYPE_DELETE; -import static com.android.systemui.screenshot.ScreenshotController.EXTRA_ID; -import static com.android.systemui.screenshot.ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED; -import static com.android.systemui.screenshot.ScreenshotController.SCREENSHOT_URI_ID; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Environment; -import android.provider.MediaStore; -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.io.File; -import java.util.concurrent.Executor; - -@RunWith(AndroidTestingRunner.class) -@SmallTest -public class DeleteScreenshotReceiverTest extends SysuiTestCase { - - @Mock - private ScreenshotSmartActions mMockScreenshotSmartActions; - @Mock - private Executor mMockExecutor; - - private DeleteScreenshotReceiver mDeleteScreenshotReceiver; - private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mDeleteScreenshotReceiver = - new DeleteScreenshotReceiver(mMockScreenshotSmartActions, mMockExecutor); - } - - @Test - public void testNoUriProvided() { - Intent intent = new Intent(mContext, DeleteScreenshotReceiver.class); - - mDeleteScreenshotReceiver.onReceive(mContext, intent); - - verify(mMockExecutor, never()).execute(any(Runnable.class)); - verify(mMockScreenshotSmartActions, never()).notifyScreenshotAction( - any(String.class), any(String.class), anyBoolean(), - any(Intent.class)); - } - - @Test - public void testFileDeleted() { - DeleteScreenshotReceiver deleteScreenshotReceiver = - new DeleteScreenshotReceiver(mMockScreenshotSmartActions, mFakeExecutor); - ContentResolver contentResolver = mContext.getContentResolver(); - final Uri testUri = contentResolver.insert( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, getFakeContentValues()); - assertNotNull(testUri); - - try { - Cursor cursor = - contentResolver.query(testUri, null, null, null, null); - assertEquals(1, cursor.getCount()); - Intent intent = new Intent(mContext, DeleteScreenshotReceiver.class) - .putExtra(SCREENSHOT_URI_ID, testUri.toString()); - - deleteScreenshotReceiver.onReceive(mContext, intent); - int runCount = mFakeExecutor.runAllReady(); - - assertEquals(1, runCount); - cursor = - contentResolver.query(testUri, null, null, null, null); - assertEquals(0, cursor.getCount()); - } finally { - contentResolver.delete(testUri, null, null); - } - - // ensure smart actions not called by default - verify(mMockScreenshotSmartActions, never()).notifyScreenshotAction( - any(String.class), any(String.class), anyBoolean(), any(Intent.class)); - } - - @Test - public void testNotifyScreenshotAction() { - Intent intent = new Intent(mContext, DeleteScreenshotReceiver.class); - String uriString = "testUri"; - String testId = "testID"; - intent.putExtra(SCREENSHOT_URI_ID, uriString); - intent.putExtra(EXTRA_ID, testId); - intent.putExtra(EXTRA_SMART_ACTIONS_ENABLED, true); - - mDeleteScreenshotReceiver.onReceive(mContext, intent); - - verify(mMockExecutor).execute(any(Runnable.class)); - verify(mMockScreenshotSmartActions).notifyScreenshotAction(testId, - ACTION_TYPE_DELETE, false, null); - } - - private static ContentValues getFakeContentValues() { - final ContentValues values = new ContentValues(); - values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES - + File.separator + Environment.DIRECTORY_SCREENSHOTS); - values.put(MediaStore.MediaColumns.DISPLAY_NAME, "test_screenshot"); - values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png"); - values.put(MediaStore.MediaColumns.DATE_ADDED, 0); - values.put(MediaStore.MediaColumns.DATE_MODIFIED, 0); - return values; - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt index 2f911fffe335..92c240404b24 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotSoundControllerTest.kt @@ -22,8 +22,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import java.lang.IllegalStateException +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -31,12 +33,14 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify @SmallTest +@OptIn(ExperimentalCoroutinesApi::class) class ScreenshotSoundControllerTest : SysuiTestCase() { private val soundProvider = mock<ScreenshotSoundProvider>() private val mediaPlayer = mock<MediaPlayer>() private val bgDispatcher = UnconfinedTestDispatcher() private val scope = TestScope(bgDispatcher) + @Before fun setup() { whenever(soundProvider.getScreenshotSound()).thenReturn(mediaPlayer) @@ -45,52 +49,59 @@ class ScreenshotSoundControllerTest : SysuiTestCase() { @Test fun init_soundLoading() { createController() - bgDispatcher.scheduler.runCurrent() + scope.advanceUntilIdle() verify(soundProvider).getScreenshotSound() } @Test - fun init_soundLoadingException_playAndReleaseDoNotThrow() = runTest { - whenever(soundProvider.getScreenshotSound()).thenThrow(IllegalStateException()) + fun init_soundLoadingException_playAndReleaseDoNotThrow() = + scope.runTest { + whenever(soundProvider.getScreenshotSound()).thenThrow(IllegalStateException()) - val controller = createController() + val controller = createController() - controller.playCameraSound().await() - controller.releaseScreenshotSound().await() + controller.playScreenshotSound() + advanceUntilIdle() - verify(mediaPlayer, never()).start() - verify(mediaPlayer, never()).release() - } + verify(mediaPlayer, never()).start() + verify(mediaPlayer, never()).release() + } @Test - fun playCameraSound_soundLoadingSuccessful_mediaPlayerPlays() = runTest { - val controller = createController() + fun playCameraSound_soundLoadingSuccessful_mediaPlayerPlays() = + scope.runTest { + val controller = createController() - controller.playCameraSound().await() + controller.playScreenshotSound() + advanceUntilIdle() - verify(mediaPlayer).start() - } + verify(mediaPlayer).start() + } @Test - fun playCameraSound_illegalStateException_doesNotThrow() = runTest { - whenever(mediaPlayer.start()).thenThrow(IllegalStateException()) + fun playCameraSound_illegalStateException_doesNotThrow() = + scope.runTest { + whenever(mediaPlayer.start()).thenThrow(IllegalStateException()) - val controller = createController() - controller.playCameraSound().await() + val controller = createController() + controller.playScreenshotSound() + advanceUntilIdle() - verify(mediaPlayer).start() - verify(mediaPlayer).release() - } + verify(mediaPlayer).start() + verify(mediaPlayer).release() + } @Test - fun playCameraSound_soundLoadingSuccessful_mediaPlayerReleases() = runTest { - val controller = createController() + fun playCameraSound_soundLoadingSuccessful_mediaPlayerReleases() = + scope.runTest { + val controller = createController() - controller.releaseScreenshotSound().await() + controller.releaseScreenshotSound() + advanceUntilIdle() - verify(mediaPlayer).release() - } + verify(mediaPlayer).release() + } private fun createController() = ScreenshotSoundControllerImpl(soundProvider, scope, bgDispatcher) diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt index 3dc90374206a..0baee5dd0e9d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt @@ -274,8 +274,8 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback) screenshotExecutor.onCloseSystemDialogsReceived() - verify(controller0).dismissScreenshot(any()) - verify(controller1).dismissScreenshot(any()) + verify(controller0).requestDismissal(any()) + verify(controller1).requestDismissal(any()) screenshotExecutor.onDestroy() } @@ -290,8 +290,8 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback) screenshotExecutor.onCloseSystemDialogsReceived() - verify(controller0, never()).dismissScreenshot(any()) - verify(controller1).dismissScreenshot(any()) + verify(controller0, never()).requestDismissal(any()) + verify(controller1).requestDismissal(any()) screenshotExecutor.onDestroy() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeSessionTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/scroll/FakeSessionTest.java index 4c8a4b0f8f61..aad461392cf6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeSessionTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/scroll/FakeSessionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import static com.google.common.util.concurrent.Futures.getUnchecked; diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureClientTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/scroll/ScrollCaptureClientTest.java index 670a130d610a..10232602b655 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureClientTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/scroll/ScrollCaptureClientTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import static org.junit.Assert.assertEquals; @@ -37,8 +37,8 @@ import android.view.ScrollCaptureResponse; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.screenshot.ScrollCaptureClient.CaptureResult; -import com.android.systemui.screenshot.ScrollCaptureClient.Session; +import com.android.systemui.screenshot.scroll.ScrollCaptureClient.CaptureResult; +import com.android.systemui.screenshot.scroll.ScrollCaptureClient.Session; import com.google.common.util.concurrent.ListenableFuture; diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java index 6f081c759df7..f39f5439d4ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/scroll/ScrollCaptureControllerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import static com.google.common.util.concurrent.Futures.getUnchecked; import static com.google.common.util.concurrent.Futures.immediateFuture; @@ -36,7 +36,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.systemui.SysuiTestCase; -import com.android.systemui.screenshot.ScrollCaptureClient.Session; +import com.android.systemui.screenshot.scroll.ScrollCaptureClient.Session; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureFrameworkSmokeTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/scroll/ScrollCaptureFrameworkSmokeTest.java index de97bc36be56..5699cfc96c8a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollCaptureFrameworkSmokeTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/scroll/ScrollCaptureFrameworkSmokeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollViewActivity.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/scroll/ScrollViewActivity.java index 4c84df2769a0..04aba1133a78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScrollViewActivity.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/scroll/ScrollViewActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import android.app.Activity; import android.os.Bundle; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 3666248d1783..a71555688e48 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -82,6 +82,9 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInte import com.android.systemui.keyguard.domain.interactor.KeyguardSurfaceBehindInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor; +import com.android.systemui.keyguard.shared.model.KeyguardState; +import com.android.systemui.keyguard.shared.model.TransitionState; +import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.TaskbarDelegate; import com.android.systemui.plugins.ActivityStarter; @@ -101,6 +104,8 @@ import com.android.systemui.util.kotlin.JavaAdapter; import com.google.common.truth.Truth; +import kotlin.Unit; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -1039,4 +1044,35 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { verify(mCentralSurfaces, never()).hideKeyguard(); verify(mPrimaryBouncerInteractor, never()).show(true); } + + @Test + public void altBouncerNotVisible_keyguardAuthenticatedBiometricsHandled() { + clearInvocations(mAlternateBouncerInteractor); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(false); + mStatusBarKeyguardViewManager.consumeKeyguardAuthenticatedBiometricsHandled(Unit.INSTANCE); + verify(mAlternateBouncerInteractor, never()).hide(); + } + + @Test + public void altBouncerVisible_keyguardAuthenticatedBiometricsHandled() { + clearInvocations(mAlternateBouncerInteractor); + when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true); + mStatusBarKeyguardViewManager.consumeKeyguardAuthenticatedBiometricsHandled(Unit.INSTANCE); + verify(mAlternateBouncerInteractor).hide(); + } + + @Test + public void fromAlternateBouncerTransitionStep() { + clearInvocations(mAlternateBouncerInteractor); + mStatusBarKeyguardViewManager.consumeFromAlternateBouncerTransitionSteps( + new TransitionStep( + /* from */ KeyguardState.ALTERNATE_BOUNCER, + /* to */ KeyguardState.GONE, + /* value */ 1f, + TransitionState.FINISHED, + "StatusBarKeyguardViewManagerTest" + ) + ); + verify(mAlternateBouncerInteractor).hide(); + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/FakeScrollCaptureConnection.java b/packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/scroll/FakeScrollCaptureConnection.java index 63f7c9755782..ea59c0a24cf8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/FakeScrollCaptureConnection.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/scroll/FakeScrollCaptureConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import android.content.pm.ActivityInfo; import android.graphics.Canvas; diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/FakeSession.java b/packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/scroll/FakeSession.java index 478658eb232d..3b7b158264cf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/FakeSession.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/scroll/FakeSession.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.screenshot; +package com.android.systemui.screenshot.scroll; import static android.util.MathUtils.constrain; @@ -32,6 +32,8 @@ import android.hardware.HardwareBuffer; import android.media.Image; import android.util.Log; +import com.android.systemui.screenshot.scroll.ScrollCaptureClient; + import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index 21a02ed2957d..dedc96e0a688 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -105,6 +105,7 @@ import android.util.IntArray; import android.util.Log; import android.util.LongSparseArray; import android.util.Pair; +import android.util.SizeF; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -159,6 +160,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBackupProvider, OnCrossProfileWidgetProvidersChangeListener { @@ -172,6 +174,9 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku private static final String STATE_FILENAME = "appwidgets.xml"; + private static final String KEY_SIZES = "sizes"; + private static final String SIZE_SEPARATOR = ","; + private static final int MIN_UPDATE_PERIOD = DEBUG ? 0 : 30 * 60 * 1000; // 30 minutes private static final int TAG_UNDEFINED = -1; @@ -2677,6 +2682,13 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku out.attributeIntHex(null, "max_height", (maxHeight > 0) ? maxHeight : 0); out.attributeIntHex(null, "host_category", widget.options.getInt( AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)); + List<SizeF> sizes = widget.options.getParcelableArrayList( + AppWidgetManager.OPTION_APPWIDGET_SIZES, SizeF.class); + if (sizes != null) { + String sizeStr = sizes.stream().map(SizeF::toString) + .collect(Collectors.joining(SIZE_SEPARATOR)); + out.attribute(null, KEY_SIZES, sizeStr); + } if (saveRestoreCompleted) { boolean restoreCompleted = widget.options.getBoolean( AppWidgetManager.OPTION_APPWIDGET_RESTORE_COMPLETED); @@ -2708,6 +2720,17 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (maxHeight != -1) { options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, maxHeight); } + String sizesStr = parser.getAttributeValue(null, KEY_SIZES); + if (sizesStr != null) { + try { + ArrayList<SizeF> sizes = Arrays.stream(sizesStr.split(SIZE_SEPARATOR)) + .map(SizeF::parseSizeF) + .collect(Collectors.toCollection(ArrayList::new)); + options.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, sizes); + } catch (NumberFormatException e) { + Slog.e(TAG, "Error parsing widget sizes", e); + } + } int category = parser.getAttributeIntHex(null, "host_category", AppWidgetProviderInfo.WIDGET_CATEGORY_UNKNOWN); if (category != AppWidgetProviderInfo.WIDGET_CATEGORY_UNKNOWN) { diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java index 40b2f5ab852f..10030b3c9176 100644 --- a/services/core/java/com/android/server/display/BrightnessRangeController.java +++ b/services/core/java/com/android/server/display/BrightnessRangeController.java @@ -21,6 +21,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.display.brightness.clamper.HdrClamper; import com.android.server.display.feature.DisplayManagerFlags; @@ -30,8 +31,7 @@ import java.util.function.BooleanSupplier; class BrightnessRangeController { private final HighBrightnessModeController mHbmController; - private final NormalBrightnessModeController mNormalBrightnessModeController = - new NormalBrightnessModeController(); + private final NormalBrightnessModeController mNormalBrightnessModeController; private final HdrClamper mHdrClamper; @@ -45,17 +45,21 @@ class BrightnessRangeController { Runnable modeChangeCallback, DisplayDeviceConfig displayDeviceConfig, Handler handler, DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) { this(hbmController, modeChangeCallback, displayDeviceConfig, + new NormalBrightnessModeController(), new HdrClamper(modeChangeCallback::run, new Handler(handler.getLooper())), flags, displayToken, info); } + @VisibleForTesting BrightnessRangeController(HighBrightnessModeController hbmController, Runnable modeChangeCallback, DisplayDeviceConfig displayDeviceConfig, + NormalBrightnessModeController normalBrightnessModeController, HdrClamper hdrClamper, DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) { mHbmController = hbmController; mModeChangeCallback = modeChangeCallback; mHdrClamper = hdrClamper; + mNormalBrightnessModeController = normalBrightnessModeController; mUseHdrClamper = flags.isHdrClamperEnabled(); mUseNbmController = flags.isNbmControllerEnabled(); if (mUseNbmController) { @@ -126,8 +130,11 @@ class BrightnessRangeController { float getCurrentBrightnessMax() { - if (mUseNbmController && mHbmController.getHighBrightnessMode() - == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF) { + // nbmController might adjust maxBrightness only if device does not support HBM or + // hbm is currently not allowed + if (mUseNbmController + && (!mHbmController.deviceSupportsHbm() + || !mHbmController.isHbmCurrentlyAllowed())) { return Math.min(mHbmController.getCurrentBrightnessMax(), mNormalBrightnessModeController.getCurrentBrightnessMax()); } diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java index ab7c503bcb83..a12d2481330b 100644 --- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java +++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java @@ -42,6 +42,9 @@ import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.notifications.DisplayNotificationManager; import com.android.server.display.utils.DebugUtils; +import java.util.HashSet; +import java.util.Set; + /** * Listens for Skin thermal sensor events, disables external displays if thermal status becomes * equal or above {@link android.os.Temperature#THROTTLING_CRITICAL}, enables external displays if @@ -106,6 +109,10 @@ class ExternalDisplayPolicy { private final ExternalDisplayStatsService mExternalDisplayStatsService; @ThrottlingStatus private volatile int mStatus = THROTTLING_NONE; + //@GuardedBy("mSyncRoot") + private boolean mIsBootCompleted; + //@GuardedBy("mSyncRoot") + private final Set<Integer> mDisplayIdsWaitingForBootCompletion = new HashSet<>(); ExternalDisplayPolicy(@NonNull final Injector injector) { mInjector = injector; @@ -121,6 +128,17 @@ class ExternalDisplayPolicy { * Starts listening for temperature changes. */ void onBootCompleted() { + synchronized (mSyncRoot) { + mIsBootCompleted = true; + for (var displayId : mDisplayIdsWaitingForBootCompletion) { + var logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId); + if (logicalDisplay != null) { + handleExternalDisplayConnectedLocked(logicalDisplay); + } + } + mDisplayIdsWaitingForBootCompletion.clear(); + } + if (!mFlags.isConnectedDisplayManagementEnabled()) { if (DEBUG) { Slog.d(TAG, "External display management is not enabled on your device:" @@ -189,6 +207,11 @@ class ExternalDisplayPolicy { return; } + if (!mIsBootCompleted) { + mDisplayIdsWaitingForBootCompletion.add(logicalDisplay.getDisplayIdLocked()); + return; + } + mExternalDisplayStatsService.onDisplayConnected(logicalDisplay); if ((Build.IS_ENG || Build.IS_USERDEBUG) @@ -227,7 +250,12 @@ class ExternalDisplayPolicy { return; } - mExternalDisplayStatsService.onDisplayDisconnected(logicalDisplay.getDisplayIdLocked()); + var displayId = logicalDisplay.getDisplayIdLocked(); + if (mDisplayIdsWaitingForBootCompletion.remove(displayId)) { + return; + } + + mExternalDisplayStatsService.onDisplayDisconnected(displayId); } /** diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java index a9f78fd5bb2a..47176fe331bf 100644 --- a/services/core/java/com/android/server/display/HighBrightnessModeController.java +++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java @@ -168,7 +168,7 @@ class HighBrightnessModeController { } float getCurrentBrightnessMax() { - if (!deviceSupportsHbm() || isCurrentlyAllowed()) { + if (!deviceSupportsHbm() || isHbmCurrentlyAllowed()) { // Either the device doesn't support HBM, or HBM range is currently allowed (device // it in a high-lux environment). In either case, return the highest brightness // level supported by the device. @@ -356,7 +356,7 @@ class HighBrightnessModeController { return event.getStartTimeMillis(); } - private boolean isCurrentlyAllowed() { + boolean isHbmCurrentlyAllowed() { // Returns true if HBM is allowed (above the ambient lux threshold) and there's still // time within the current window for additional HBM usage. We return false if there is an // HDR layer because we don't want the brightness MAX to change for HDR, which has its @@ -369,7 +369,7 @@ class HighBrightnessModeController { && !mIsBlockedByLowPowerMode); } - private boolean deviceSupportsHbm() { + boolean deviceSupportsHbm() { return mHbmData != null && mHighBrightnessModeMetadata != null; } @@ -462,7 +462,7 @@ class HighBrightnessModeController { + ", isOnlyAllowedToStayOn: " + isOnlyAllowedToStayOn + ", remainingAllowedTime: " + remainingTime + ", isLuxHigh: " + mIsInAllowedAmbientRange - + ", isHBMCurrentlyAllowed: " + isCurrentlyAllowed() + + ", isHBMCurrentlyAllowed: " + isHbmCurrentlyAllowed() + ", isHdrLayerPresent: " + mIsHdrLayerPresent + ", mMaxDesiredHdrSdrRatio: " + mMaxDesiredHdrSdrRatio + ", isAutoBrightnessEnabled: " + mIsAutoBrightnessEnabled @@ -575,7 +575,7 @@ class HighBrightnessModeController { return BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF; } else if (mIsHdrLayerPresent) { return BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR; - } else if (isCurrentlyAllowed()) { + } else if (isHbmCurrentlyAllowed()) { return BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT; } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 186cf5e37003..ef9acc4b41ae 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -3598,7 +3598,8 @@ final class InstallPackageHelper { continue; } if ((scanFlags & SCAN_DROP_CACHE) != 0) { - final PackageCacher cacher = new PackageCacher(mPm.getCacheDir()); + final PackageCacher cacher = new PackageCacher(mPm.getCacheDir(), + mPm.mPackageParserCallback); Log.w(TAG, "Dropping cache of " + file.getAbsolutePath()); cacher.cleanCachedResult(file); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index fe8030b656b0..c814a1ef1c13 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -1741,7 +1741,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService () -> LocalServices.getService(UserManagerInternal.class)), (i, pm) -> new DisplayMetrics(), (i, pm) -> new PackageParser2(pm.mSeparateProcesses, i.getDisplayMetrics(), - new PackageCacher(pm.mCacheDir), + new PackageCacher(pm.mCacheDir, pm.mPackageParserCallback), pm.mPackageParserCallback) /* scanningCachingPackageParserProducer */, (i, pm) -> new PackageParser2(pm.mSeparateProcesses, i.getDisplayMetrics(), null, pm.mPackageParserCallback) /* scanningPackageParserProducer */, diff --git a/services/core/java/com/android/server/pm/parsing/PackageCacher.java b/services/core/java/com/android/server/pm/parsing/PackageCacher.java index b6267c499c07..2db454aa4c41 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageCacher.java +++ b/services/core/java/com/android/server/pm/parsing/PackageCacher.java @@ -17,6 +17,7 @@ package com.android.server.pm.parsing; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.pm.PackageParserCacheHelper; import android.os.Environment; import android.os.FileUtils; @@ -29,8 +30,10 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.pm.parsing.IPackageCacher; +import com.android.internal.pm.parsing.PackageParser2; import com.android.internal.pm.parsing.pkg.PackageImpl; import com.android.internal.pm.parsing.pkg.ParsedPackage; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; import com.android.server.pm.ApexManager; import libcore.io.IoUtils; @@ -51,9 +54,16 @@ public class PackageCacher implements IPackageCacher { @NonNull private final File mCacheDir; + @Nullable + private final PackageParser2.Callback mCallback; - public PackageCacher(@NonNull File cacheDir) { + public PackageCacher(File cacheDir) { + this(cacheDir, null); + } + + public PackageCacher(File cacheDir, @Nullable PackageParser2.Callback callback) { this.mCacheDir = cacheDir; + this.mCallback = callback; } /** @@ -71,12 +81,17 @@ public class PackageCacher implements IPackageCacher { @VisibleForTesting protected ParsedPackage fromCacheEntry(byte[] bytes) { - return fromCacheEntryStatic(bytes); + return fromCacheEntryStatic(bytes, mCallback); } /** static version of {@link #fromCacheEntry} for unit tests. */ @VisibleForTesting public static ParsedPackage fromCacheEntryStatic(byte[] bytes) { + return fromCacheEntryStatic(bytes, null); + } + + private static ParsedPackage fromCacheEntryStatic(byte[] bytes, + @Nullable ParsingPackageUtils.Callback callback) { final Parcel p = Parcel.obtain(); p.unmarshall(bytes, 0, bytes.length); p.setDataPosition(0); @@ -85,7 +100,7 @@ public class PackageCacher implements IPackageCacher { new PackageParserCacheHelper.ReadHelper(p); helper.startAndInstall(); - ParsedPackage pkg = new PackageImpl(p); + ParsedPackage pkg = new PackageImpl(p, callback); p.recycle(); diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java index b18503d7d5cb..1c70af0a56ea 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -596,7 +596,7 @@ public class PackageInfoUtils { ai.requiredDisplayCategory = a.getRequiredDisplayCategory(); ai.requireContentUriPermissionFromCaller = a.getRequireContentUriPermissionFromCaller(); ai.setKnownActivityEmbeddingCerts(a.getKnownActivityEmbeddingCerts()); - assignFieldsComponentInfoParsedMainComponent(ai, a, pkgSetting, state, userId); + assignFieldsComponentInfoParsedMainComponent(ai, a, pkgSetting, userId); return ai; } @@ -659,7 +659,7 @@ public class PackageInfoUtils { // Backwards compatibility, coerce to null if empty si.metaData = metaData.isEmpty() ? null : metaData; } - assignFieldsComponentInfoParsedMainComponent(si, s, pkgSetting, state, userId); + assignFieldsComponentInfoParsedMainComponent(si, s, pkgSetting, userId); return si; } @@ -710,7 +710,7 @@ public class PackageInfoUtils { pi.metaData = metaData.isEmpty() ? null : metaData; } pi.applicationInfo = applicationInfo; - assignFieldsComponentInfoParsedMainComponent(pi, p, pkgSetting, state, userId); + assignFieldsComponentInfoParsedMainComponent(pi, p, pkgSetting, userId); return pi; } @@ -903,13 +903,8 @@ public class PackageInfoUtils { private static void assignFieldsComponentInfoParsedMainComponent( @NonNull ComponentInfo info, @NonNull ParsedMainComponent component, - @NonNull PackageStateInternal pkgSetting, @NonNull PackageUserStateInternal state, - @UserIdInt int userId) { + @NonNull PackageStateInternal pkgSetting, @UserIdInt int userId) { assignFieldsComponentInfoParsedMainComponent(info, component); - // overwrite the enabled state with the current user state - info.enabled = PackageUserStateUtils.isEnabled(state, info.applicationInfo.enabled, - info.enabled, info.name, /* flags */ 0); - Pair<CharSequence, Integer> labelAndIcon = ParsedComponentStateUtils.getNonLocalizedLabelAndIcon(component, pkgSetting, userId); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index ec4b38b10af2..994f50cc1f07 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -530,6 +530,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { // TODO(b/178103325): Track sleep/requested sleep for every display. volatile boolean mRequestedOrSleepingDefaultDisplay; + /** + * This is used to check whether to invoke {@link #updateScreenOffSleepToken} when screen is + * turned off. E.g. if it is false when screen is turned off and the display is swapping, it + * is expected that the screen will be on in a short time. Then it is unnecessary to acquire + * screen-off-sleep-token, so it can avoid intermediate visibility or lifecycle changes. + */ + volatile boolean mIsGoingToSleepDefaultDisplay; + volatile boolean mRecentsVisible; volatile boolean mNavBarVirtualKeyHapticFeedbackEnabled = true; volatile boolean mPictureInPictureVisible; @@ -5464,6 +5472,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { } mRequestedOrSleepingDefaultDisplay = true; + mIsGoingToSleepDefaultDisplay = true; + + // In case startedGoingToSleep is called after screenTurnedOff (the source caller is in + // order but the methods run on different threads) and updateScreenOffSleepToken was + // skipped. Then acquire sleep token if screen was off. + if (!mDefaultDisplayPolicy.isScreenOnFully() && !mDefaultDisplayPolicy.isScreenOnEarly() + && com.android.window.flags.Flags.skipSleepingWhenSwitchingDisplay()) { + updateScreenOffSleepToken(true /* acquire */, false /* isSwappingDisplay */); + } if (mKeyguardDelegate != null) { mKeyguardDelegate.onStartedGoingToSleep(pmSleepReason); @@ -5487,6 +5504,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { MetricsLogger.histogram(mContext, "screen_timeout", mLockScreenTimeout / 1000); mRequestedOrSleepingDefaultDisplay = false; + mIsGoingToSleepDefaultDisplay = false; mDefaultDisplayPolicy.setAwake(false); // We must get this work done here because the power manager will drop @@ -5522,7 +5540,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } EventLogTags.writeScreenToggled(1); - + mIsGoingToSleepDefaultDisplay = false; mDefaultDisplayPolicy.setAwake(true); // Since goToSleep performs these functions synchronously, we must @@ -5624,7 +5642,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (DEBUG_WAKEUP) Slog.i(TAG, "Display" + displayId + " turned off..."); if (displayId == DEFAULT_DISPLAY) { - updateScreenOffSleepToken(true, isSwappingDisplay); + if (!isSwappingDisplay || mIsGoingToSleepDefaultDisplay + || !com.android.window.flags.Flags.skipSleepingWhenSwitchingDisplay()) { + updateScreenOffSleepToken(true /* acquire */, isSwappingDisplay); + } mRequestedOrSleepingDefaultDisplay = false; mDefaultDisplayPolicy.screenTurnedOff(); synchronized (mLock) { diff --git a/services/tests/displayservicetests/src/com/android/server/display/BrightnessRangeControllerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/BrightnessRangeControllerTest.kt new file mode 100644 index 000000000000..1f3184d056f2 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessRangeControllerTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display + +import android.os.IBinder +import androidx.test.filters.SmallTest +import com.android.server.display.brightness.clamper.HdrClamper +import com.android.server.display.feature.DisplayManagerFlags +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +private const val MAX_BRIGHTNESS = 1.0f +private const val TRANSITION_POINT = 0.7f +private const val NORMAL_BRIGHTNESS_HIGH = 0.8f +private const val NORMAL_BRIGHTNESS_LOW = 0.6f + +@SmallTest +class BrightnessRangeControllerTest { + + private val mockHbmController = mock<HighBrightnessModeController>() + private val mockCallback = mock<Runnable>() + private val mockConfig = mock<DisplayDeviceConfig>() + private val mockNormalBrightnessController = mock<NormalBrightnessModeController>() + private val mockHdrClamper = mock<HdrClamper>() + private val mockFlags = mock<DisplayManagerFlags>() + private val mockToken = mock<IBinder>() + + @Test + fun `returns HBC max brightness if HBM supported and ON`() { + val controller = createController() + assertThat(controller.currentBrightnessMax).isEqualTo(MAX_BRIGHTNESS) + } + + @Test + fun `returns NBC max brightness if device does not support HBM`() { + val controller = createController(hbmSupported = false) + assertThat(controller.currentBrightnessMax).isEqualTo(NORMAL_BRIGHTNESS_LOW) + } + + @Test + fun `returns NBC max brightness if HBM not allowed`() { + val controller = createController(hbmAllowed = false) + assertThat(controller.currentBrightnessMax).isEqualTo(NORMAL_BRIGHTNESS_LOW) + } + + @Test + fun `returns HBC max brightness if NBM is disabled`() { + val controller = createController(nbmEnabled = false, hbmAllowed = false) + assertThat(controller.currentBrightnessMax).isEqualTo(MAX_BRIGHTNESS) + } + + @Test + fun `returns HBC max brightness if lower than NBC max brightness`() { + val controller = createController( + hbmAllowed = false, + hbmMaxBrightness = TRANSITION_POINT, + nbmMaxBrightness = NORMAL_BRIGHTNESS_HIGH + ) + assertThat(controller.currentBrightnessMax).isEqualTo(TRANSITION_POINT) + } + + private fun createController( + nbmEnabled: Boolean = true, + hbmSupported: Boolean = true, + hbmAllowed: Boolean = true, + hbmMaxBrightness: Float = MAX_BRIGHTNESS, + nbmMaxBrightness: Float = NORMAL_BRIGHTNESS_LOW + ): BrightnessRangeController { + whenever(mockFlags.isNbmControllerEnabled).thenReturn(nbmEnabled) + whenever(mockHbmController.deviceSupportsHbm()).thenReturn(hbmSupported) + whenever(mockHbmController.isHbmCurrentlyAllowed).thenReturn(hbmAllowed) + whenever(mockHbmController.currentBrightnessMax).thenReturn(hbmMaxBrightness) + whenever(mockNormalBrightnessController.currentBrightnessMax).thenReturn(nbmMaxBrightness) + + return BrightnessRangeController(mockHbmController, mockCallback, mockConfig, + mockNormalBrightnessController, mockHdrClamper, mockFlags, mockToken, + DisplayDeviceInfo()) + } +}
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index b142334db9e9..18f03113c01c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -2408,6 +2408,7 @@ public class DisplayManagerServiceTest { when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); DisplayManagerInternal localService = displayManager.new LocalService(); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -2440,6 +2441,7 @@ public class DisplayManagerServiceTest { .when(() -> SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); DisplayManagerInternal localService = displayManager.new LocalService(); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -2487,6 +2489,7 @@ public class DisplayManagerServiceTest { when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); @@ -2652,6 +2655,7 @@ public class DisplayManagerServiceTest { when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); DisplayManagerService.BinderService bs = displayManager.new BinderService(); DisplayManagerInternal localService = displayManager.new LocalService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -2699,6 +2703,7 @@ public class DisplayManagerServiceTest { when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index e9315c8ed8e6..ba41d31b5a47 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -1850,6 +1850,8 @@ public final class DisplayPowerControllerTest { mock(ScreenOffBrightnessSensorController.class); final HighBrightnessModeController hbmController = mock(HighBrightnessModeController.class); final HdrClamper hdrClamper = mock(HdrClamper.class); + final NormalBrightnessModeController normalBrightnessModeController = mock( + NormalBrightnessModeController.class); BrightnessClamperController clamperController = mock(BrightnessClamperController.class); when(hbmController.getCurrentBrightnessMax()).thenReturn(PowerManager.BRIGHTNESS_MAX); @@ -1862,7 +1864,8 @@ public final class DisplayPowerControllerTest { TestInjector injector = spy(new TestInjector(displayPowerState, animator, automaticBrightnessController, wakelockController, brightnessMappingStrategy, - hysteresisLevels, screenOffBrightnessSensorController, hbmController, hdrClamper, + hysteresisLevels, screenOffBrightnessSensorController, + hbmController, normalBrightnessModeController, hdrClamper, clamperController, mDisplayManagerFlagsMock)); final LogicalDisplay display = mock(LogicalDisplay.class); @@ -1950,6 +1953,8 @@ public final class DisplayPowerControllerTest { private final ScreenOffBrightnessSensorController mScreenOffBrightnessSensorController; private final HighBrightnessModeController mHighBrightnessModeController; + private final NormalBrightnessModeController mNormalBrightnessModeController; + private final HdrClamper mHdrClamper; private final BrightnessClamperController mClamperController; @@ -1963,6 +1968,7 @@ public final class DisplayPowerControllerTest { HysteresisLevels hysteresisLevels, ScreenOffBrightnessSensorController screenOffBrightnessSensorController, HighBrightnessModeController highBrightnessModeController, + NormalBrightnessModeController normalBrightnessModeController, HdrClamper hdrClamper, BrightnessClamperController clamperController, DisplayManagerFlags flags) { @@ -1974,6 +1980,7 @@ public final class DisplayPowerControllerTest { mHysteresisLevels = hysteresisLevels; mScreenOffBrightnessSensorController = screenOffBrightnessSensorController; mHighBrightnessModeController = highBrightnessModeController; + mNormalBrightnessModeController = normalBrightnessModeController; mHdrClamper = hdrClamper; mClamperController = clamperController; mFlags = flags; @@ -2086,7 +2093,8 @@ public final class DisplayPowerControllerTest { DisplayDeviceConfig displayDeviceConfig, Handler handler, DisplayManagerFlags flags, IBinder displayToken, DisplayDeviceInfo info) { return new BrightnessRangeController(hbmController, modeChangeCallback, - displayDeviceConfig, mHdrClamper, mFlags, displayToken, info); + displayDeviceConfig, mNormalBrightnessModeController, mHdrClamper, + mFlags, displayToken, info); } @Override diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java index 1529a087c284..1a71e77a3b1b 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java @@ -228,13 +228,27 @@ public class ExternalDisplayPolicyTest { @Test public void testOnExternalDisplayAvailable() { - when(mMockedLogicalDisplay.isEnabledLocked()).thenReturn(false); + mExternalDisplayPolicy.handleExternalDisplayConnectedLocked(mMockedLogicalDisplay); + assertNotAskedToEnableDisplay(); + verify(mMockedExternalDisplayStatsService, never()).onDisplayConnected(any()); + + mExternalDisplayPolicy.onBootCompleted(); assertAskedToEnableDisplay(); verify(mMockedExternalDisplayStatsService).onDisplayConnected(eq(mMockedLogicalDisplay)); } @Test + public void testOnExternalDisplayUnpluggedBeforeBootCompletes() { + mExternalDisplayPolicy.handleExternalDisplayConnectedLocked(mMockedLogicalDisplay); + mExternalDisplayPolicy.handleLogicalDisplayDisconnectedLocked(mMockedLogicalDisplay); + mExternalDisplayPolicy.onBootCompleted(); + assertNotAskedToEnableDisplay(); + verify(mMockedExternalDisplayStatsService, never()).onDisplayConnected(any()); + verify(mMockedExternalDisplayStatsService, never()).onDisplayDisconnected(anyInt()); + } + + @Test public void testOnExternalDisplayAvailable_criticalThermalCondition() throws RemoteException { // Disallow external displays due to thermals. @@ -303,8 +317,14 @@ public class ExternalDisplayPolicyTest { mDisplayEventCaptor.capture()); assertThat(mLogicalDisplayCaptor.getValue()).isEqualTo(mMockedLogicalDisplay); assertThat(mDisplayEventCaptor.getValue()).isEqualTo(EVENT_DISPLAY_CONNECTED); + verify(mMockedLogicalDisplay).setEnabledLocked(false); clearInvocations(mMockedLogicalDisplayMapper); - when(mMockedLogicalDisplay.isEnabledLocked()).thenReturn(true); + clearInvocations(mMockedLogicalDisplay); + } + + private void assertNotAskedToEnableDisplay() { + verify(mMockedInjector, never()).sendExternalDisplayEventLocked(any(), anyInt()); + verify(mMockedLogicalDisplay, never()).setEnabledLocked(anyBoolean()); } private void assertIsExternalDisplayAllowed(final boolean enabled) { diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java index 29467f259ac3..a80e2f8ae28c 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java @@ -16,10 +16,14 @@ package com.android.server.policy; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManagerGlobal.ADD_OKAY; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; @@ -33,18 +37,27 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import android.app.ActivityManager; import android.app.AppOpsManager; +import android.content.Context; +import android.os.PowerManager; import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.filters.SmallTest; +import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.DisplayPolicy; +import com.android.server.wm.DisplayRotation; +import com.android.server.wm.WindowManagerInternal; import org.junit.After; import org.junit.Before; @@ -64,16 +77,27 @@ public class PhoneWindowManagerTests { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); PhoneWindowManager mPhoneWindowManager; + private ActivityTaskManagerInternal mAtmInternal; + private Context mContext; @Before public void setUp() { mPhoneWindowManager = spy(new PhoneWindowManager()); spyOn(ActivityManager.getService()); + mContext = getInstrumentation().getTargetContext(); + spyOn(mContext); + mAtmInternal = mock(ActivityTaskManagerInternal.class); + LocalServices.addService(ActivityTaskManagerInternal.class, mAtmInternal); + mPhoneWindowManager.mActivityTaskManagerInternal = mAtmInternal; + LocalServices.addService(WindowManagerInternal.class, mock(WindowManagerInternal.class)); } @After public void tearDown() { reset(ActivityManager.getService()); + reset(mContext); + LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class); + LocalServices.removeServiceForTest(WindowManagerInternal.class); } @Test @@ -99,6 +123,60 @@ public class PhoneWindowManagerTests { } @Test + public void testScreenTurnedOff() { + mSetFlagsRule.enableFlags(com.android.window.flags.Flags + .FLAG_SKIP_SLEEPING_WHEN_SWITCHING_DISPLAY); + doNothing().when(mPhoneWindowManager).updateSettings(any()); + doNothing().when(mPhoneWindowManager).initializeHdmiState(); + final boolean[] isScreenTurnedOff = { false }; + final DisplayPolicy displayPolicy = mock(DisplayPolicy.class); + doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff(); + doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnEarly(); + doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnFully(); + + mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy; + mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class); + final ActivityTaskManagerInternal.SleepTokenAcquirer tokenAcquirer = + mock(ActivityTaskManagerInternal.SleepTokenAcquirer.class); + doReturn(tokenAcquirer).when(mAtmInternal).createSleepTokenAcquirer(anyString()); + final PowerManager pm = mock(PowerManager.class); + doReturn(true).when(pm).isInteractive(); + doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE)); + + mContext.getMainThreadHandler().runWithScissors(() -> mPhoneWindowManager.init( + new PhoneWindowManager.Injector(mContext, + mock(WindowManagerPolicy.WindowManagerFuncs.class))), 0); + assertThat(isScreenTurnedOff[0]).isFalse(); + assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse(); + + // Skip sleep-token for non-sleep-screen-off. + clearInvocations(tokenAcquirer); + mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); + verify(tokenAcquirer, never()).acquire(anyInt(), anyBoolean()); + assertThat(isScreenTurnedOff[0]).isTrue(); + + // Apply sleep-token for sleep-screen-off. + mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); + assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isTrue(); + mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); + verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY), eq(true)); + + mPhoneWindowManager.finishedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); + assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse(); + + // Simulate unexpected reversed order: screenTurnedOff -> startedGoingToSleep. The sleep + // token can still be acquired. + isScreenTurnedOff[0] = false; + clearInvocations(tokenAcquirer); + mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); + verify(tokenAcquirer, never()).acquire(anyInt(), anyBoolean()); + assertThat(displayPolicy.isScreenOnEarly()).isFalse(); + assertThat(displayPolicy.isScreenOnFully()).isFalse(); + mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); + verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY), eq(false)); + } + + @Test public void testCheckAddPermission_withoutAccessibilityOverlay_noAccessibilityAppOpLogged() { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); @@ -130,11 +208,8 @@ public class PhoneWindowManagerTests { private void mockStartDockOrHome() throws Exception { doNothing().when(ActivityManager.getService()).stopAppSwitches(); - ActivityTaskManagerInternal mMockActivityTaskManagerInternal = - mock(ActivityTaskManagerInternal.class); - when(mMockActivityTaskManagerInternal.startHomeOnDisplay( + when(mAtmInternal.startHomeOnDisplay( anyInt(), anyString(), anyInt(), anyBoolean(), anyBoolean())).thenReturn(false); - mPhoneWindowManager.mActivityTaskManagerInternal = mMockActivityTaskManagerInternal; mPhoneWindowManager.mUserManagerInternal = mock(UserManagerInternal.class); } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 5a5296836089..ae4faa84421e 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -16,6 +16,7 @@ package com.android.server.voiceinteraction; +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION; import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; @@ -1072,8 +1073,10 @@ public class VoiceInteractionManagerService extends SystemService { // If visEnabledKey is set to true (or absent), we try following VIS path. String csPkgName = mContext.getResources() .getString(R.string.config_defaultContextualSearchPackageName); - if (!csPkgName.equals(getCurInteractor( - Binder.getCallingUserHandle().getIdentifier()).getPackageName())) { + ComponentName currInteractor = + getCurInteractor(Binder.getCallingUserHandle().getIdentifier()); + if (currInteractor == null + || !csPkgName.equals(currInteractor.getPackageName())) { // Check if the interactor can handle Contextual Search. // If not, return failure. Slog.w(TAG, "Contextual Search not supported yet. Returning failure."); @@ -2718,7 +2721,7 @@ public class VoiceInteractionManagerService extends SystemService { } launchIntent.setComponent(resolveInfo.getComponentInfo().getComponentName()); launchIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NO_ANIMATION - | FLAG_ACTIVITY_NO_USER_ACTION); + | FLAG_ACTIVITY_NO_USER_ACTION | FLAG_ACTIVITY_CLEAR_TASK); launchIntent.putExtras(args); boolean isAssistDataAllowed = mAtmInternal.isAssistDataAllowed(); final List<ActivityAssistInfo> records = mAtmInternal.getTopVisibleActivities(); diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 8f7619a7dbdb..cb56cc2a5e20 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -7987,6 +7987,27 @@ public class CarrierConfigManager { KEY_SCAN_LIMITED_SERVICE_AFTER_VOLTE_FAILURE_BOOL = KEY_PREFIX + "scan_limited_service_after_volte_failure_bool"; + /** + * This config defines {@link ImsReasonInfo} code with which the emergency call + * shall be retried. + * + * <p> + * If the reason code is one of the following, the emergency call shall be retried + * regardless of this configuration. + * <ul> + * <li>{@link ImsReasonInfo#CODE_LOCAL_CALL_CS_RETRY_REQUIRED}</li> + * <li>{@link ImsReasonInfo#CODE_LOCAL_NOT_REGISTERED}</li> + * <li>{@link ImsReasonInfo#CODE_SIP_ALTERNATE_EMERGENCY_CALL}</li> + * </ul> + * <p> + * + * This config is empty by default. + * + * @hide + */ + public static final String KEY_IMS_REASONINFO_CODE_TO_RETRY_EMERGENCY_INT_ARRAY = + KEY_PREFIX + "ims_reasoninfo_code_to_retry_emergency_int_array"; + private static PersistableBundle getDefaults() { PersistableBundle defaults = new PersistableBundle(); defaults.putBoolean(KEY_RETRY_EMERGENCY_ON_IMS_PDN_BOOL, false); @@ -8059,6 +8080,8 @@ public class CarrierConfigManager { defaults.putBoolean(KEY_START_QUICK_CROSS_STACK_REDIAL_TIMER_WHEN_REGISTERED_BOOL, true); defaults.putBoolean(KEY_SCAN_LIMITED_SERVICE_AFTER_VOLTE_FAILURE_BOOL, false); + defaults.putIntArray(KEY_IMS_REASONINFO_CODE_TO_RETRY_EMERGENCY_INT_ARRAY, + new int[0]); return defaults; } @@ -9828,6 +9851,19 @@ public class CarrierConfigManager { "satellite_entitlement_supported_bool"; /** + * Indicates the appName that is used when querying the entitlement server for satellite. + * + * The default value is androidSatmode. + * + * Reference: GSMA TS.43-v11, 2.8.5 Fast Authentication and Token Management. + * `app_name` is an optional attribute in the request and may vary depending on the carrier + * requirement. + * @hide + */ + public static final String KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING = + "satellite_entitlement_app_name_string"; + + /** * Indicating whether DUN APN should be disabled when the device is roaming. In that case, * the default APN (i.e. internet) will be used for tethering. * @@ -10945,6 +10981,7 @@ public class CarrierConfigManager { sDefaults.putBoolean(KEY_OVERRIDE_WFC_ROAMING_MODE_WHILE_USING_NTN_BOOL, true); sDefaults.putInt(KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT, 7); sDefaults.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false); + sDefaults.putString(KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING, "androidSatmode"); sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false); sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, ""); sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false); |