diff options
| author | 2024-03-26 02:32:04 +0000 | |
|---|---|---|
| committer | 2024-03-26 02:32:04 +0000 | |
| commit | 7548f9fa58e38c706d8056682da5725dfce2a05f (patch) | |
| tree | f8760e953b4a3174900e8b53a909f55e3d1c43fd | |
| parent | 3f1150397574b188d6be19626f6d149029ac3052 (diff) | |
| parent | 6b2d650d06546f5ea4986c24e59423c82341e332 (diff) | |
Merge changes I45ede2eb,Ie802e1fe,If7be9b12,I33e66103,I520affe2, ... into 24D1-dev
* changes:
Screenshot shelf (xml version)
Remove obsolete classes ActionProxyReceiver and DeleteScreenshotReceiver
Move screenshot scroll capture code into its own package
Use AssistedFactory for ScreenshotViewProxy
Move more code into ScreenshotViewProxy
Move dismissal logic into the ScreenshotViewProxy
Simplify ScreenshotController/ViewProxy interface
Abstract out surface between ScreenshotController and ScreenshotView
Switch to using withContext in ScreenshotSoundController
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; |