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

Commit 6d9b3216 authored by Andre Le's avatar Andre Le Committed by Android (Google) Code Review
Browse files

Merge changes I85d4517f,I5231db4c into main

* changes:
  QSDetailedView: Move listener and createOptionsView to view binder
  QSDetailedView: Move init views and start record callback to view binder
parents 75b1ac70 724194f4
Loading
Loading
Loading
Loading
+2 −23
Original line number Diff line number Diff line
@@ -22,10 +22,8 @@ import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewStub
import android.view.WindowManager
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.Spinner
@@ -33,7 +31,6 @@ import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.res.R
@@ -48,7 +45,7 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>(
    @DrawableRes private val dialogIconDrawable: Int? = null,
    @ColorRes private val dialogIconTint: Int? = null,
    @ScreenShareMode val defaultSelectedMode: Int = screenShareOptions.first().mode,
) : DialogDelegate<T>, AdapterView.OnItemSelectedListener {
) : DialogDelegate<T> {
    private lateinit var dialogTitle: TextView
    private lateinit var cancelButton: TextView
    private lateinit var screenShareModeSpinner: Spinner
@@ -84,7 +81,6 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>(
        dialogTitle = dialog.requireViewById(R.id.screen_share_dialog_title)
        cancelButton = dialog.requireViewById(android.R.id.button2)
        updateIcon()
        createOptionsView(getOptionsViewLayoutId())
        if (!::viewBinder.isInitialized) {
            viewBinder = createViewBinder()
        }
@@ -106,7 +102,7 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>(
        val adapter = OptionsAdapter(dialog.context.applicationContext, screenShareOptions)
        screenShareModeSpinner = dialog.requireViewById(R.id.screen_share_mode_options)
        screenShareModeSpinner.adapter = adapter
        screenShareModeSpinner.onItemSelectedListener = this
        screenShareModeSpinner.onItemSelectedListener = viewBinder

        // disable redundant Touch & Hold accessibility action for Switch Access
        screenShareModeSpinner.accessibilityDelegate =
@@ -124,12 +120,6 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>(
        screenShareModeSpinner.setSelection(defaultModePosition, /* animate= */ false)
    }

    override fun onItemSelected(adapterView: AdapterView<*>?, view: View, pos: Int, id: Long) {
        viewBinder.onItemSelected(pos)
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {}

    fun getSelectedScreenShareOption(): ScreenShareOption {
        return viewBinder.selectedScreenShareOption
    }
@@ -147,17 +137,6 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>(
    protected fun setCancelButtonOnClickListener(listener: View.OnClickListener?) {
        cancelButton.setOnClickListener(listener)
    }

    // Create additional options that is shown under the share mode spinner
    // Eg. the audio and tap toggles in SysUI Recorder
    @LayoutRes protected open fun getOptionsViewLayoutId(): Int? = null

    private fun createOptionsView(@LayoutRes layoutId: Int?) {
        if (layoutId == null) return
        val stub = dialog.requireViewById<View>(R.id.options_stub) as ViewStub
        stub.layoutResource = layoutId
        stub.inflate()
    }
}

private class OptionsAdapter(context: Context, private val options: List<ScreenShareOption>) :
+19 −2
Original line number Diff line number Diff line
@@ -18,7 +18,10 @@ package com.android.systemui.mediaprojection.permission

import android.app.AlertDialog
import android.view.View
import android.view.ViewStub
import android.widget.AdapterView
import android.widget.TextView
import androidx.annotation.LayoutRes
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.res.R

@@ -29,7 +32,7 @@ open class BaseMediaProjectionPermissionViewBinder(
    private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
    @ScreenShareMode val defaultSelectedMode: Int = screenShareOptions.first().mode,
    private val dialog: AlertDialog,
) {
) : AdapterView.OnItemSelectedListener {
    private lateinit var warning: TextView
    private lateinit var startButton: TextView
    var selectedScreenShareOption: ScreenShareOption =
@@ -48,6 +51,7 @@ open class BaseMediaProjectionPermissionViewBinder(
        warning = dialog.requireViewById(R.id.text_warning)
        startButton = dialog.requireViewById(android.R.id.button1)
        initScreenShareOptions()
        createOptionsView(getOptionsViewLayoutId())
    }

    private fun initScreenShareOptions() {
@@ -61,11 +65,13 @@ open class BaseMediaProjectionPermissionViewBinder(
        startButton.text = startButtonText
    }

    open fun onItemSelected(pos: Int) {
    override fun onItemSelected(adapterView: AdapterView<*>?, view: View, pos: Int, id: Long) {
        selectedScreenShareOption = screenShareOptions[pos]
        setOptionSpecificFields()
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {}

    private val warningText: String
        get() = dialog.context.getString(selectedScreenShareOption.warningText, appName)

@@ -78,4 +84,15 @@ open class BaseMediaProjectionPermissionViewBinder(
            listener?.onClick(view)
        }
    }

    // Create additional options that is shown under the share mode spinner
    // Eg. the audio and tap toggles in SysUI Recorder
    @LayoutRes protected open fun getOptionsViewLayoutId(): Int? = null

    private fun createOptionsView(@LayoutRes layoutId: Int?) {
        if (layoutId == null) return
        val stub = dialog.requireViewById<View>(R.id.options_stub) as ViewStub
        stub.layoutResource = layoutId
        stub.inflate()
    }
}
+5 −160
Original line number Diff line number Diff line
@@ -15,34 +15,15 @@
 */
package com.android.systemui.screenrecord

import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.hardware.display.DisplayManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.ResultReceiver
import android.os.UserHandle
import android.view.Display
import android.view.MotionEvent.ACTION_MOVE
import android.view.View
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.Switch
import androidx.annotation.LayoutRes
import androidx.annotation.StyleRes
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity
import com.android.systemui.mediaprojection.permission.BaseMediaProjectionPermissionDialogDelegate
import com.android.systemui.mediaprojection.permission.BaseMediaProjectionPermissionViewBinder
import com.android.systemui.mediaprojection.permission.ENTIRE_SCREEN
import com.android.systemui.mediaprojection.permission.SINGLE_APP
import com.android.systemui.mediaprojection.permission.ScreenShareMode
import com.android.systemui.plugins.ActivityStarter
@@ -115,17 +96,18 @@ class ScreenRecordPermissionDialogDelegate(
        ): ScreenRecordPermissionDialogDelegate
    }

    private lateinit var tapsSwitch: Switch
    private lateinit var audioSwitch: Switch
    private lateinit var options: Spinner

    override fun createViewBinder(): BaseMediaProjectionPermissionViewBinder {
        return ScreenRecordPermissionViewBinder(
            hostUserHandle,
            hostUid,
            mediaProjectionMetricsLogger,
            defaultSelectedMode,
            displayManager,
            dialog,
            controller,
            activityStarter,
            userContextProvider,
            onStartRecordingClicked,
        )
    }

@@ -137,143 +119,6 @@ class ScreenRecordPermissionDialogDelegate(
        super<BaseMediaProjectionPermissionDialogDelegate>.onCreate(dialog, savedInstanceState)
        setDialogTitle(R.string.screenrecord_permission_dialog_title)
        dialog.setTitle(R.string.screenrecord_title)
        setStartButtonOnClickListener { v: View? ->
            onStartRecordingClicked?.run()
            val selectedScreenShareOption = getSelectedScreenShareOption()
            if (selectedScreenShareOption.mode == ENTIRE_SCREEN) {
                requestScreenCapture(/* captureTarget= */ null, selectedScreenShareOption.displayId)
            }
            if (selectedScreenShareOption.mode == SINGLE_APP) {
                val intent = Intent(dialog.context, MediaProjectionAppSelectorActivity::class.java)
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

                // We can't start activity for result here so we use result receiver to get
                // the selected target to capture
                intent.putExtra(
                    MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
                    CaptureTargetResultReceiver(),
                )

                intent.putExtra(
                    MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
                    hostUserHandle,
                )
                intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, hostUid)
                intent.putExtra(
                    MediaProjectionAppSelectorActivity.EXTRA_SCREEN_SHARE_TYPE,
                    MediaProjectionAppSelectorActivity.ScreenShareType.ScreenRecord.name,
                )
                activityStarter.startActivity(intent, /* dismissShade= */ true)
            }
            dialog.dismiss()
        }
        setCancelButtonOnClickListener { dialog.dismiss() }
        initRecordOptionsView()
    }

    @LayoutRes override fun getOptionsViewLayoutId(): Int = R.layout.screen_record_options

    @SuppressLint("ClickableViewAccessibility")
    private fun initRecordOptionsView() {
        // TODO(b/378514312): Move this function to ScreenRecordPermissionViewBinder
        audioSwitch = dialog.requireViewById(R.id.screenrecord_audio_switch)
        tapsSwitch = dialog.requireViewById(R.id.screenrecord_taps_switch)

        // Add these listeners so that the switch only responds to movement
        // within its target region, to meet accessibility requirements
        audioSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE }
        tapsSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE }

        options = dialog.requireViewById(R.id.screen_recording_options)
        val a: ArrayAdapter<*> =
            ScreenRecordingAdapter(
                dialog.context,
                android.R.layout.simple_spinner_dropdown_item,
                MODES,
            )
        a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        options.adapter = a
        options.setOnItemClickListenerInt { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
            audioSwitch.isChecked = true
        }

        // disable redundant Touch & Hold accessibility action for Switch Access
        options.accessibilityDelegate =
            object : View.AccessibilityDelegate() {
                override fun onInitializeAccessibilityNodeInfo(
                    host: View,
                    info: AccessibilityNodeInfo,
                ) {
                    info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK)
                    super.onInitializeAccessibilityNodeInfo(host, info)
                }
            }
        options.isLongClickable = false
    }

    /**
     * Starts screen capture after some countdown
     *
     * @param captureTarget target to capture (could be e.g. a task) or null to record the whole
     *   screen
     */
    private fun requestScreenCapture(
        captureTarget: MediaProjectionCaptureTarget?,
        displayId: Int = Display.DEFAULT_DISPLAY,
    ) {
        val userContext = userContextProvider.userContext
        val showTaps = getSelectedScreenShareOption().mode != SINGLE_APP && tapsSwitch.isChecked
        val audioMode =
            if (audioSwitch.isChecked) options.selectedItem as ScreenRecordingAudioSource
            else ScreenRecordingAudioSource.NONE
        val startIntent =
            PendingIntent.getForegroundService(
                userContext,
                RecordingService.REQUEST_CODE,
                RecordingService.getStartIntent(
                    userContext,
                    Activity.RESULT_OK,
                    audioMode.ordinal,
                    showTaps,
                    displayId,
                    captureTarget,
                ),
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
            )
        val stopIntent =
            PendingIntent.getService(
                userContext,
                RecordingService.REQUEST_CODE,
                RecordingService.getStopIntent(userContext),
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
            )
        controller.startCountdown(DELAY_MS, INTERVAL_MS, startIntent, stopIntent)
    }

    private inner class CaptureTargetResultReceiver :
        ResultReceiver(Handler(Looper.getMainLooper())) {
        override fun onReceiveResult(resultCode: Int, resultData: Bundle) {
            if (resultCode == Activity.RESULT_OK) {
                val captureTarget =
                    resultData.getParcelable(
                        MediaProjectionAppSelectorActivity.KEY_CAPTURE_TARGET,
                        MediaProjectionCaptureTarget::class.java,
                    )

                // Start recording of the selected target
                requestScreenCapture(captureTarget)
            }
        }
    }

    companion object {
        private val MODES =
            listOf(
                ScreenRecordingAudioSource.INTERNAL,
                ScreenRecordingAudioSource.MIC,
                ScreenRecordingAudioSource.MIC_AND_INTERNAL,
            )
        private const val DELAY_MS: Long = 3000
        private const val INTERVAL_MS: Long = 1000
    }
}
+162 −3
Original line number Diff line number Diff line
@@ -17,27 +17,51 @@
package com.android.systemui.screenrecord

import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.app.PendingIntent
import android.content.Intent
import android.hardware.display.DisplayManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.ResultReceiver
import android.os.UserHandle
import android.view.Display
import android.view.MotionEvent.ACTION_MOVE
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import android.widget.Switch
import androidx.annotation.LayoutRes
import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity
import com.android.systemui.mediaprojection.permission.BaseMediaProjectionPermissionViewBinder
import com.android.systemui.mediaprojection.permission.ENTIRE_SCREEN
import com.android.systemui.mediaprojection.permission.SINGLE_APP
import com.android.systemui.mediaprojection.permission.ScreenShareMode
import com.android.systemui.mediaprojection.permission.ScreenShareOption
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.settings.UserContextProvider

class ScreenRecordPermissionViewBinder(
    hostUid: Int,
    private val hostUserHandle: UserHandle,
    private val hostUid: Int,
    mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
    @ScreenShareMode defaultSelectedMode: Int,
    displayManager: DisplayManager,
    private val dialog: AlertDialog,
    private val controller: RecordingController,
    private val activityStarter: ActivityStarter,
    private val userContextProvider: UserContextProvider,
    private val onStartRecordingClicked: Runnable?,
) :
    BaseMediaProjectionPermissionViewBinder(
        createOptionList(displayManager),
@@ -47,21 +71,90 @@ class ScreenRecordPermissionViewBinder(
        defaultSelectedMode,
        dialog,
    ) {
    private lateinit var tapsSwitch: Switch
    private lateinit var audioSwitch: Switch
    private lateinit var tapsView: View
    private lateinit var options: Spinner

    override fun bind() {
        super.bind()
        initRecordOptionsView()
        setStartButtonOnClickListener { _: View? ->
            onStartRecordingClicked?.run()
            if (selectedScreenShareOption.mode == ENTIRE_SCREEN) {
                requestScreenCapture(
                    captureTarget = null,
                    displayId = selectedScreenShareOption.displayId,
                )
            }
            if (selectedScreenShareOption.mode == SINGLE_APP) {
                val intent = Intent(dialog.context, MediaProjectionAppSelectorActivity::class.java)
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

                // We can't start activity for result here so we use result receiver to get
                // the selected target to capture
                intent.putExtra(
                    MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
                    CaptureTargetResultReceiver(),
                )

                intent.putExtra(
                    MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
                    hostUserHandle,
                )
                intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, hostUid)
                intent.putExtra(
                    MediaProjectionAppSelectorActivity.EXTRA_SCREEN_SHARE_TYPE,
                    MediaProjectionAppSelectorActivity.ScreenShareType.ScreenRecord.name,
                )
                activityStarter.startActivity(intent, /* dismissShade= */ true)
            }
            dialog.dismiss()
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    private fun initRecordOptionsView() {
        audioSwitch = dialog.requireViewById(R.id.screenrecord_audio_switch)
        tapsSwitch = dialog.requireViewById(R.id.screenrecord_taps_switch)

        tapsView = dialog.requireViewById(R.id.show_taps)
        updateTapsViewVisibility()

        // Add these listeners so that the switch only responds to movement
        // within its target region, to meet accessibility requirements
        audioSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE }
        tapsSwitch.setOnTouchListener { _, event -> event.action == ACTION_MOVE }

        options = dialog.requireViewById(R.id.screen_recording_options)
        val a: ArrayAdapter<*> =
            ScreenRecordingAdapter(
                dialog.context,
                android.R.layout.simple_spinner_dropdown_item,
                MODES,
            )
        a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        options.adapter = a
        options.setOnItemClickListenerInt { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
            audioSwitch.isChecked = true
        }

        // disable redundant Touch & Hold accessibility action for Switch Access
        options.accessibilityDelegate =
            object : View.AccessibilityDelegate() {
                override fun onInitializeAccessibilityNodeInfo(
                    host: View,
                    info: AccessibilityNodeInfo,
                ) {
                    info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK)
                    super.onInitializeAccessibilityNodeInfo(host, info)
                }
            }
        options.isLongClickable = false
    }

    override fun onItemSelected(pos: Int) {
        super.onItemSelected(pos)
    override fun onItemSelected(adapterView: AdapterView<*>?, view: View, pos: Int, id: Long) {
        super.onItemSelected(adapterView, view, pos, id)
        updateTapsViewVisibility()
    }

@@ -69,7 +162,73 @@ class ScreenRecordPermissionViewBinder(
        tapsView.visibility = if (selectedScreenShareOption.mode == SINGLE_APP) GONE else VISIBLE
    }

    @LayoutRes override fun getOptionsViewLayoutId(): Int = R.layout.screen_record_options

    /**
     * Starts screen capture after some countdown
     *
     * @param captureTarget target to capture (could be e.g. a task) or null to record the whole
     *   screen
     */
    private fun requestScreenCapture(
        captureTarget: MediaProjectionCaptureTarget?,
        displayId: Int = Display.DEFAULT_DISPLAY,
    ) {
        val userContext = userContextProvider.userContext
        val showTaps = selectedScreenShareOption.mode != SINGLE_APP && tapsSwitch.isChecked
        val audioMode =
            if (audioSwitch.isChecked) options.selectedItem as ScreenRecordingAudioSource
            else ScreenRecordingAudioSource.NONE
        val startIntent =
            PendingIntent.getForegroundService(
                userContext,
                RecordingService.REQUEST_CODE,
                RecordingService.getStartIntent(
                    userContext,
                    Activity.RESULT_OK,
                    audioMode.ordinal,
                    showTaps,
                    displayId,
                    captureTarget,
                ),
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
            )
        val stopIntent =
            PendingIntent.getService(
                userContext,
                RecordingService.REQUEST_CODE,
                RecordingService.getStopIntent(userContext),
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
            )
        controller.startCountdown(DELAY_MS, INTERVAL_MS, startIntent, stopIntent)
    }

    private inner class CaptureTargetResultReceiver :
        ResultReceiver(Handler(Looper.getMainLooper())) {
        override fun onReceiveResult(resultCode: Int, resultData: Bundle) {
            if (resultCode == Activity.RESULT_OK) {
                val captureTarget =
                    resultData.getParcelable(
                        MediaProjectionAppSelectorActivity.KEY_CAPTURE_TARGET,
                        MediaProjectionCaptureTarget::class.java,
                    )

                // Start recording of the selected target
                requestScreenCapture(captureTarget)
            }
        }
    }

    companion object {
        private val MODES =
            listOf(
                ScreenRecordingAudioSource.INTERNAL,
                ScreenRecordingAudioSource.MIC,
                ScreenRecordingAudioSource.MIC_AND_INTERNAL,
            )

        private const val DELAY_MS: Long = 3000
        private const val INTERVAL_MS: Long = 1000

        private val RECORDABLE_DISPLAY_TYPES =
            intArrayOf(