Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8d0c72b3 authored by Miranda Kephart's avatar Miranda Kephart
Browse files

Screenshot shelf (xml version)

Bug: 329659738
Test: manual
Flag: ACONFIG com.android.systemui.screenshot_shelf_ui DEVELOPMENT

Change-Id: I45ede2ebcdcff7e3229028494c319f5aa9189f2e
parent c4d4a9e5
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -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"
+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>
+2 −0
Original line number Diff line number Diff line
@@ -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] -->
+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)
    }
}
+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