diff options
52 files changed, 1397 insertions, 784 deletions
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/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/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/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/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/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; |