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

Commit a55d96f8 authored by Stefan Andonian's avatar Stefan Andonian
Browse files

Add Screenrecord to Record Issue QS Tile.

This includes unit tests, a permission dialog, and an error dialog,
all responding to user events.

Bug: 305049544
Flag: ACONFIG record_issue_qs_tile DEVELOPMENT
Test: Manually tested on device.
Screenshots:
	Blocked Dialog: https://screenshot.googleplex.com/5rf6syCys4wHApm.png
	Permission Dialog: https://screenshot.googleplex.com/5xsWRo98ULWJy3x.png
Change-Id: Id8081ba4700dc92803206209a09331f5936b71e7
parent 723bdf57
Loading
Loading
Loading
Loading
+3 −5
Original line number Diff line number Diff line
@@ -42,9 +42,7 @@ import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.recordissue.RecordIssueDialogDelegate
import com.android.systemui.res.R
import com.android.systemui.settings.UserContextProvider
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.policy.KeyguardStateController
import javax.inject.Inject

@@ -63,8 +61,7 @@ constructor(
    private val keyguardDismissUtil: KeyguardDismissUtil,
    private val keyguardStateController: KeyguardStateController,
    private val dialogLaunchAnimator: DialogLaunchAnimator,
    private val sysuiDialogFactory: SystemUIDialog.Factory,
    private val userContextProvider: UserContextProvider,
    private val delegateFactory: RecordIssueDialogDelegate.Factory,
) :
    QSTileImpl<QSTile.BooleanState>(
        host,
@@ -102,7 +99,8 @@ constructor(

    private fun showPrompt(view: View?) {
        val dialog: AlertDialog =
            RecordIssueDialogDelegate(sysuiDialogFactory, userContextProvider) {
            delegateFactory
                .create {
                    isRecording = true
                    refreshState()
                }
+85 −2
Original line number Diff line number Diff line
@@ -24,27 +24,63 @@ import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Bundle
import android.os.UserHandle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.Button
import android.widget.PopupMenu
import android.widget.Switch
import androidx.annotation.AnyThread
import androidx.annotation.MainThread
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.flags.Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.mediaprojection.SessionCreationSource
import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver
import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog
import com.android.systemui.qs.tiles.RecordIssueTile
import com.android.systemui.res.R
import com.android.systemui.screenrecord.RecordingService
import com.android.systemui.screenrecord.ScreenRecordingAudioSource
import com.android.systemui.settings.UserContextProvider
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.phone.SystemUIDialog
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.concurrent.Executor

class RecordIssueDialogDelegate(
class RecordIssueDialogDelegate
@AssistedInject
constructor(
    private val factory: SystemUIDialog.Factory,
    private val userContextProvider: UserContextProvider,
    private val onStarted: Runnable
    private val userTracker: UserTracker,
    private val flags: FeatureFlagsClassic,
    @Background private val bgExecutor: Executor,
    @Main private val mainExecutor: Executor,
    private val devicePolicyResolver: dagger.Lazy<ScreenCaptureDevicePolicyResolver>,
    private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
    private val userFileManager: UserFileManager,
    @Assisted private val onStarted: Runnable,
) : SystemUIDialog.Delegate {

    /** To inject dependencies and allow for easier testing */
    @AssistedFactory
    interface Factory {
        /** Create a dialog object */
        fun create(onStarted: Runnable): RecordIssueDialogDelegate
    }

    @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch
    private lateinit var issueTypeButton: Button

    @MainThread
    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
        dialog.apply {
            setView(LayoutInflater.from(context).inflate(R.layout.record_issue_dialog, null))
@@ -63,17 +99,64 @@ class RecordIssueDialogDelegate(

    override fun createDialog(): SystemUIDialog = factory.create(this)

    @MainThread
    override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
        dialog.apply {
            window?.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS)
            window?.setGravity(Gravity.CENTER)

            screenRecordSwitch = requireViewById(R.id.screenrecord_switch)
            screenRecordSwitch.setOnCheckedChangeListener { _, isEnabled ->
                onScreenRecordSwitchClicked(context, isEnabled)
            }
            issueTypeButton = requireViewById(R.id.issue_type_button)
            issueTypeButton.setOnClickListener { onIssueTypeClicked(context) }
        }
    }

    @AnyThread
    private fun onScreenRecordSwitchClicked(context: Context, isEnabled: Boolean) {
        if (!isEnabled) return

        bgExecutor.execute {
            if (
                flags.isEnabled(WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES) &&
                    devicePolicyResolver
                        .get()
                        .isScreenCaptureCompletelyDisabled(UserHandle.of(userTracker.userId))
            ) {
                mainExecutor.execute {
                    ScreenCaptureDisabledDialog(context).show()
                    screenRecordSwitch.isChecked = false
                }
                return@execute
            }

            mediaProjectionMetricsLogger.notifyProjectionInitiated(
                userTracker.userId,
                SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER
            )

            if (flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)) {
                val prefs =
                    userFileManager.getSharedPreferences(
                        RecordIssueTile.TILE_SPEC,
                        Context.MODE_PRIVATE,
                        userTracker.userId
                    )
                if (!prefs.getBoolean(HAS_APPROVED_SCREEN_RECORDING, false)) {
                    mainExecutor.execute {
                        ScreenCapturePermissionDialogDelegate(factory, prefs).createDialog().apply {
                            setOnCancelListener { screenRecordSwitch.isChecked = false }
                            show()
                        }
                    }
                }
            }
        }
    }

    @MainThread
    private fun onIssueTypeClicked(context: Context) {
        val selectedCategory = issueTypeButton.text.toString()
        val popupMenu = PopupMenu(context, issueTypeButton)
+49 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.recordissue

import android.content.SharedPreferences
import android.os.Bundle
import android.view.Gravity
import android.view.WindowManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialog

const val HAS_APPROVED_SCREEN_RECORDING = "HasApprovedScreenRecord"

class ScreenCapturePermissionDialogDelegate(
    private val dialogFactory: SystemUIDialog.Factory,
    private val sharedPreferences: SharedPreferences,
) : SystemUIDialog.Delegate {

    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
        dialog.apply {
            setIcon(R.drawable.ic_screenrecord)
            setTitle(R.string.screenrecord_permission_dialog_title)
            setMessage(R.string.screenrecord_permission_dialog_warning_entire_screen)
            setNegativeButton(R.string.slice_permission_deny) { _, _ -> cancel() }
            setPositiveButton(R.string.slice_permission_allow) { _, _ ->
                sharedPreferences.edit().putBoolean(HAS_APPROVED_SCREEN_RECORDING, true).apply()
                dismiss()
            }
            window?.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS)
            window?.setGravity(Gravity.CENTER)
        }
    }

    override fun createDialog(): SystemUIDialog = dialogFactory.create(this)
}
+6 −6
Original line number Diff line number Diff line
@@ -30,8 +30,8 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.recordissue.RecordIssueDialogDelegate
import com.android.systemui.res.R
import com.android.systemui.settings.UserContextProvider
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -65,9 +65,9 @@ class RecordIssueTileTest : SysuiTestCase() {
    @Mock private lateinit var keyguardDismissUtil: KeyguardDismissUtil
    @Mock private lateinit var keyguardStateController: KeyguardStateController
    @Mock private lateinit var dialogLauncherAnimator: DialogLaunchAnimator
    @Mock private lateinit var dialogFactory: SystemUIDialog.Factory
    @Mock private lateinit var delegateFactory: RecordIssueDialogDelegate.Factory
    @Mock private lateinit var dialogDelegate: RecordIssueDialogDelegate
    @Mock private lateinit var dialog: SystemUIDialog
    @Mock private lateinit var userContextProvider: UserContextProvider

    private lateinit var testableLooper: TestableLooper
    private lateinit var tile: RecordIssueTile
@@ -76,7 +76,8 @@ class RecordIssueTileTest : SysuiTestCase() {
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        whenever(host.context).thenReturn(mContext)
        whenever(dialogFactory.create(any())).thenReturn(dialog)
        whenever(delegateFactory.create(any())).thenReturn(dialogDelegate)
        whenever(dialogDelegate.createDialog()).thenReturn(dialog)

        testableLooper = TestableLooper.get(this)
        tile =
@@ -93,8 +94,7 @@ class RecordIssueTileTest : SysuiTestCase() {
                keyguardDismissUtil,
                keyguardStateController,
                dialogLauncherAnimator,
                dialogFactory,
                userContextProvider,
                delegateFactory,
            )
    }

+153 −13
Original line number Diff line number Diff line
@@ -17,6 +17,9 @@
package com.android.systemui.recordissue

import android.app.Dialog
import android.content.Context
import android.content.SharedPreferences
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.widget.Button
@@ -25,48 +28,107 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.mediaprojection.SessionCreationSource
import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver
import com.android.systemui.model.SysUiState
import com.android.systemui.qs.tiles.RecordIssueTile
import com.android.systemui.res.R
import com.android.systemui.settings.UserContextProvider
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.SystemUIDialogManager
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class RecordIssueDialogDelegateTest : SysuiTestCase() {

    @Mock private lateinit var flags: FeatureFlagsClassic
    @Mock private lateinit var devicePolicyResolver: ScreenCaptureDevicePolicyResolver
    @Mock private lateinit var dprLazy: dagger.Lazy<ScreenCaptureDevicePolicyResolver>
    @Mock private lateinit var mediaProjectionMetricsLogger: MediaProjectionMetricsLogger
    @Mock private lateinit var userContextProvider: UserContextProvider
    @Mock private lateinit var userTracker: UserTracker
    @Mock private lateinit var userFileManager: UserFileManager
    @Mock private lateinit var sharedPreferences: SharedPreferences

    @Mock private lateinit var sysuiState: SysUiState
    @Mock private lateinit var systemUIDialogManager: SystemUIDialogManager
    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
    @Mock private lateinit var bgExecutor: Executor
    @Mock private lateinit var mainExecutor: Executor
    @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator

    private lateinit var dialog: SystemUIDialog
    private lateinit var factory: SystemUIDialog.Factory
    private lateinit var latch: CountDownLatch

    @Before
    fun setup() {
        val dialogFactory =
        MockitoAnnotations.initMocks(this)
        whenever(dprLazy.get()).thenReturn(devicePolicyResolver)
        whenever(sysuiState.setFlag(anyInt(), anyBoolean())).thenReturn(sysuiState)
        whenever(userContextProvider.userContext).thenReturn(mContext)
        whenever(
                userFileManager.getSharedPreferences(
                    eq(RecordIssueTile.TILE_SPEC),
                    eq(Context.MODE_PRIVATE),
                    anyInt()
                )
            )
            .thenReturn(sharedPreferences)

        factory =
            spy(
                SystemUIDialog.Factory(
                    context,
                mock<FeatureFlags>(),
                mock<SystemUIDialogManager>(),
                mock<SysUiState>().apply {
                    whenever(setFlag(anyInt(), anyBoolean())).thenReturn(this)
                },
                mock<BroadcastDispatcher>(),
                mock<DialogLaunchAnimator>()
                    flags,
                    systemUIDialogManager,
                    sysuiState,
                    broadcastDispatcher,
                    dialogLaunchAnimator
                )
            )

        latch = CountDownLatch(1)
        dialog =
            RecordIssueDialogDelegate(dialogFactory, mock()) { latch.countDown() }.createDialog()
            RecordIssueDialogDelegate(
                    factory,
                    userContextProvider,
                    userTracker,
                    flags,
                    bgExecutor,
                    mainExecutor,
                    dprLazy,
                    mediaProjectionMetricsLogger,
                    userFileManager,
                ) {
                    latch.countDown()
                }
                .createDialog()
        dialog.show()
    }

@@ -91,4 +153,82 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() {
        dialog.getButton(Dialog.BUTTON_POSITIVE).callOnClick()
        latch.await(1L, TimeUnit.MILLISECONDS)
    }

    @Test
    fun screenCaptureDisabledDialog_isShown_whenFunctionalityIsDisabled() {
        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES))
            .thenReturn(true)
        whenever(devicePolicyResolver.isScreenCaptureCompletelyDisabled(any<UserHandle>()))
            .thenReturn(true)

        val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch)
        screenRecordSwitch.isChecked = true

        val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java)
        verify(bgExecutor).execute(bgCaptor.capture())
        bgCaptor.value.run()

        val mainCaptor = ArgumentCaptor.forClass(Runnable::class.java)
        verify(mainExecutor).execute(mainCaptor.capture())
        mainCaptor.value.run()

        verify(mediaProjectionMetricsLogger, never())
            .notifyProjectionInitiated(
                anyInt(),
                eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
            )
        assertThat(screenRecordSwitch.isChecked).isFalse()
    }

    @Test
    fun screenCapturePermissionDialog_isShown_correctly() {
        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES))
            .thenReturn(false)
        whenever(devicePolicyResolver.isScreenCaptureCompletelyDisabled(any<UserHandle>()))
            .thenReturn(false)
        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(true)
        whenever(sharedPreferences.getBoolean(HAS_APPROVED_SCREEN_RECORDING, false))
            .thenReturn(false)

        val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch)
        screenRecordSwitch.isChecked = true

        val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java)
        verify(bgExecutor).execute(bgCaptor.capture())
        bgCaptor.value.run()

        val mainCaptor = ArgumentCaptor.forClass(Runnable::class.java)
        verify(mainExecutor).execute(mainCaptor.capture())
        mainCaptor.value.run()

        verify(mediaProjectionMetricsLogger)
            .notifyProjectionInitiated(
                anyInt(),
                eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
            )
        verify(factory).create(any<ScreenCapturePermissionDialogDelegate>())
    }

    @Test
    fun noDialogsAreShown_forScreenRecord_whenApprovalIsAlreadyGiven() {
        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES))
            .thenReturn(false)
        whenever(devicePolicyResolver.isScreenCaptureCompletelyDisabled(any<UserHandle>()))
            .thenReturn(false)
        whenever(flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)).thenReturn(false)

        val screenRecordSwitch = dialog.requireViewById<Switch>(R.id.screenrecord_switch)
        screenRecordSwitch.isChecked = true

        val bgCaptor = ArgumentCaptor.forClass(Runnable::class.java)
        verify(bgExecutor).execute(bgCaptor.capture())
        bgCaptor.value.run()

        verify(mediaProjectionMetricsLogger)
            .notifyProjectionInitiated(
                anyInt(),
                eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
            )
        verify(factory, never()).create(any<ScreenCapturePermissionDialogDelegate>())
    }
}