Loading packages/SystemUI/aconfig/systemui.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -423,6 +423,13 @@ flag { bug: "327613051" } 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" Loading packages/SystemUI/res/layout/screenshot_shelf.xml 0 → 100644 +160 −0 Original line number Diff line number Diff line <?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> packages/SystemUI/res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -235,6 +235,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] --> Loading packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt 0 → 100644 +71 −0 Original line number Diff line number Diff line /* * 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) } } packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt 0 → 100644 +226 −0 Original line number Diff line number Diff line /* * 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 } } Loading
packages/SystemUI/aconfig/systemui.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -423,6 +423,13 @@ flag { bug: "327613051" } 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" Loading
packages/SystemUI/res/layout/screenshot_shelf.xml 0 → 100644 +160 −0 Original line number Diff line number Diff line <?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>
packages/SystemUI/res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -235,6 +235,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] --> Loading
packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt 0 → 100644 +71 −0 Original line number Diff line number Diff line /* * 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) } }
packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt 0 → 100644 +226 −0 Original line number Diff line number Diff line /* * 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 } }