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

Commit e8c68566 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[SB][Screen Chips] Add view model for screen record chip.

Bug: 332662551
Flag: com.android.systemui.status_bar_screen_sharing_chips
Test: with new flag on, verify screen record chip still works
Test: all tests in statusbar.chips package
Change-Id: Ifbb1946af012240605c4f0525c88f24cc8a1c860
parent 89076380
Loading
Loading
Loading
Loading
+30 −21
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.chips.mediaprojection.ui.view

import android.annotation.StringRes
import android.app.ActivityManager
import android.content.Context
import android.content.pm.PackageManager
import android.text.Html
@@ -41,6 +42,21 @@ constructor(
        return dialogFactory.create(delegate)
    }

    /** See other [getDialogMessage]. */
    fun getDialogMessage(
        state: MediaProjectionState.Projecting,
        @StringRes genericMessageResId: Int,
        @StringRes specificAppMessageResId: Int,
    ): CharSequence {
        val specificTaskInfo =
            if (state is MediaProjectionState.Projecting.SingleTask) {
                state.task
            } else {
                null
            }
        return getDialogMessage(specificTaskInfo, genericMessageResId, specificAppMessageResId)
    }

    /**
     * Returns the message to show in the dialog based on the specific media projection state.
     *
@@ -49,30 +65,23 @@ constructor(
     *   specify which app is currently being projected.
     */
    fun getDialogMessage(
        state: MediaProjectionState,
        specificTaskInfo: ActivityManager.RunningTaskInfo?,
        @StringRes genericMessageResId: Int,
        @StringRes specificAppMessageResId: Int,
    ): CharSequence {
        when (state) {
            // NotProjecting might happen if there's a bit of lag between when the screen recording
            // starts and when MediaProjection is aware that it's started, so handle it here just in
            // case.
            is MediaProjectionState.NotProjecting,
            is MediaProjectionState.Projecting.EntireScreen ->
        if (specificTaskInfo == null) {
            return context.getString(genericMessageResId)
            is MediaProjectionState.Projecting.SingleTask -> {
        }
        val packageName =
                    state.task.baseIntent.component?.packageName
            specificTaskInfo.baseIntent.component?.packageName
                ?: return context.getString(genericMessageResId)
                try {
        return try {
            val appInfo = packageManager.getApplicationInfo(packageName, 0)
            val appName = appInfo.loadLabel(packageManager)
                    return getSpecificAppMessageText(specificAppMessageResId, appName)
            getSpecificAppMessageText(specificAppMessageResId, appName)
        } catch (e: PackageManager.NameNotFoundException) {
            // TODO(b/332662551): Log this error.
                    return context.getString(genericMessageResId)
                }
            }
            context.getString(genericMessageResId)
        }
    }

+18 −41
Original line number Diff line number Diff line
@@ -16,22 +16,13 @@

package com.android.systemui.statusbar.chips.screenrecord.domain.interactor

import androidx.annotation.DrawableRes
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository
import com.android.systemui.res.R
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import com.android.systemui.screenrecord.data.repository.ScreenRecordRepository
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
import com.android.systemui.util.time.SystemClock
import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
@@ -41,7 +32,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/** Interactor for the screen recording chip shown in the status bar. */
// TODO(b/332662551): Convert this into a view model.
@SysUISingleton
class ScreenRecordChipInteractor
@Inject
@@ -49,11 +39,8 @@ constructor(
    @Application private val scope: CoroutineScope,
    private val screenRecordRepository: ScreenRecordRepository,
    private val mediaProjectionRepository: MediaProjectionRepository,
    private val systemClock: SystemClock,
    private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
    private val dialogTransitionAnimator: DialogTransitionAnimator,
) : OngoingActivityChipViewModel {
    override val chip: StateFlow<OngoingActivityChipModel> =
) {
    val screenRecordState: StateFlow<ScreenRecordChipModel> =
        // ScreenRecordRepository has the main "is the screen being recorded?" state, and
        // MediaProjectionRepository has information about what specifically is being recorded (a
        // single app or the entire screen)
@@ -62,37 +49,27 @@ constructor(
                mediaProjectionRepository.mediaProjectionState,
            ) { screenRecordState, mediaProjectionState ->
                when (screenRecordState) {
                    is ScreenRecordModel.DoingNothing,
                    is ScreenRecordModel.DoingNothing -> ScreenRecordChipModel.DoingNothing
                    // TODO(b/332662551): Implement the 3-2-1 countdown chip.
                    is ScreenRecordModel.Starting -> OngoingActivityChipModel.Hidden
                    is ScreenRecordModel.Recording ->
                        OngoingActivityChipModel.Shown(
                            // TODO(b/332662551): Also provide a content description.
                            icon = Icon.Resource(ICON, contentDescription = null),
                            startTimeMs = systemClock.elapsedRealtime(),
                            createDialogLaunchOnClickListener(
                                createDelegate(mediaProjectionState),
                                dialogTransitionAnimator
                            ),
                        )
                    is ScreenRecordModel.Starting ->
                        ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted)
                    is ScreenRecordModel.Recording -> {
                        val recordedTask =
                            if (
                                mediaProjectionState is MediaProjectionState.Projecting.SingleTask
                            ) {
                                mediaProjectionState.task
                            } else {
                                null
                            }
                        ScreenRecordChipModel.Recording(recordedTask)
                    }
            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
                }
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), ScreenRecordChipModel.DoingNothing)

    /** Stops the recording. */
    fun stopRecording() {
        scope.launch { screenRecordRepository.stopRecording() }
    }

    private fun createDelegate(state: MediaProjectionState): EndScreenRecordingDialogDelegate {
        return EndScreenRecordingDialogDelegate(
            endMediaProjectionDialogHelper,
            this@ScreenRecordChipInteractor,
            state,
        )
    }

    companion object {
        @DrawableRes val ICON = R.drawable.ic_screenrecord
    }
}
+38 −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.statusbar.chips.screenrecord.domain.model

import android.app.ActivityManager

/** Represents the status of screen record needed to show a chip in the status bar. */
sealed interface ScreenRecordChipModel {
    /** There's nothing related to screen recording happening. */
    data object DoingNothing : ScreenRecordChipModel

    /** A screen recording will begin in [millisUntilStarted] ms. */
    data class Starting(val millisUntilStarted: Long) : ScreenRecordChipModel

    /**
     * There's an active screen recording happening.
     *
     * @property recordedTask the task being recorded if the user is recording only a single app.
     *   Null if the user is recording the entire screen or we don't have the task info yet.
     */
    data class Recording(
        val recordedTask: ActivityManager.RunningTaskInfo?,
    ) : ScreenRecordChipModel
}
+7 −7
Original line number Diff line number Diff line
@@ -16,18 +16,18 @@

package com.android.systemui.statusbar.chips.screenrecord.ui.view

import android.app.ActivityManager
import android.os.Bundle
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel
import com.android.systemui.statusbar.phone.SystemUIDialog

/** A dialog that lets the user stop an ongoing screen recording. */
class EndScreenRecordingDialogDelegate(
    private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
    private val interactor: ScreenRecordChipInteractor,
    private val state: MediaProjectionState,
    private val stopAction: () -> Unit,
    private val recordedTask: ActivityManager.RunningTaskInfo?,
) : SystemUIDialog.Delegate {

    override fun createDialog(): SystemUIDialog {
@@ -36,11 +36,11 @@ class EndScreenRecordingDialogDelegate(

    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
        with(dialog) {
            setIcon(ScreenRecordChipInteractor.ICON)
            setIcon(ScreenRecordChipViewModel.ICON)
            setTitle(R.string.screenrecord_stop_dialog_title)
            setMessage(
                endMediaProjectionDialogHelper.getDialogMessage(
                    state,
                    recordedTask,
                    genericMessageResId = R.string.screenrecord_stop_dialog_message,
                    specificAppMessageResId = R.string.screenrecord_stop_dialog_message_specific_app
                )
@@ -49,7 +49,7 @@ class EndScreenRecordingDialogDelegate(
            // button is clicked anyway.
            setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
            setPositiveButton(R.string.screenrecord_stop_dialog_button) { _, _ ->
                interactor.stopRecording()
                stopAction.invoke()
            }
        }
    }
+87 −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.statusbar.chips.screenrecord.ui.viewmodel

import android.app.ActivityManager
import androidx.annotation.DrawableRes
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

/** View model for the screen recording chip shown in the status bar. */
@SysUISingleton
class ScreenRecordChipViewModel
@Inject
constructor(
    @Application private val scope: CoroutineScope,
    private val interactor: ScreenRecordChipInteractor,
    private val systemClock: SystemClock,
    private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
    private val dialogTransitionAnimator: DialogTransitionAnimator,
) : OngoingActivityChipViewModel {
    override val chip: StateFlow<OngoingActivityChipModel> =
        interactor.screenRecordState
            .map { state ->
                when (state) {
                    is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden
                    // TODO(b/332662551): Implement the 3-2-1 countdown chip.
                    is ScreenRecordChipModel.Starting -> OngoingActivityChipModel.Hidden
                    is ScreenRecordChipModel.Recording -> {
                        OngoingActivityChipModel.Shown(
                            // TODO(b/332662551): Also provide a content description.
                            icon = Icon.Resource(ICON, contentDescription = null),
                            startTimeMs = systemClock.elapsedRealtime(),
                            createDialogLaunchOnClickListener(
                                createDelegate(state.recordedTask),
                                dialogTransitionAnimator,
                            ),
                        )
                    }
                }
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)

    private fun createDelegate(
        recordedTask: ActivityManager.RunningTaskInfo?
    ): EndScreenRecordingDialogDelegate {
        return EndScreenRecordingDialogDelegate(
            endMediaProjectionDialogHelper,
            stopAction = interactor::stopRecording,
            recordedTask,
        )
    }

    companion object {
        @DrawableRes val ICON = R.drawable.ic_screenrecord
    }
}
Loading