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

Commit 10aa58b1 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov Committed by Android (Google) Code Review
Browse files

Merge changes I248867da,Ifbb1946a,I2e16efa0,I1bd2cc89,Id9aa09b2, ... into main

* changes:
  [SB][Screen Chips] Remove context.mainExecutor from tests.
  [SB][Screen Chips] Add view model for screen record chip.
  [SB][Screen Chips] Add view model for call chip.
  [SB][Screen Chips] Add view models for share-to-app and cast-to-other.
  [SB][Screen Chips] Show app name in screen record stop dialog if needed.
  [SB][Screen Chips] Move call chip to new chips architecture.
parents 082fe7bb ab3d4836
Loading
Loading
Loading
Loading
+2 −0
Original line number Original line Diff line number Diff line
@@ -323,6 +323,8 @@
    <string name="screenrecord_stop_dialog_title">Stop recording screen?</string>
    <string name="screenrecord_stop_dialog_title">Stop recording screen?</string>
    <!-- Text telling a user that they will stop recording their screen if they click the "Stop recording" button [CHAR LIMIT=100] -->
    <!-- Text telling a user that they will stop recording their screen if they click the "Stop recording" button [CHAR LIMIT=100] -->
    <string name="screenrecord_stop_dialog_message">You will stop recording your screen</string>
    <string name="screenrecord_stop_dialog_message">You will stop recording your screen</string>
    <!-- Text telling a user that they will stop recording the contents of the specified [app_name] if they click the "Stop recording" button. Note that the app name will appear in bold. [CHAR LIMIT=100] -->
    <string name="screenrecord_stop_dialog_message_specific_app">You will stop recording &lt;b><xliff:g id="app_name" example="Photos App">%1$s</xliff:g>&lt;/b></string>
    <!-- Button to stop a screen recording [CHAR LIMIT=35] -->
    <!-- Button to stop a screen recording [CHAR LIMIT=35] -->
    <string name="screenrecord_stop_dialog_button">Stop recording</string>
    <string name="screenrecord_stop_dialog_button">Stop recording</string>


+7 −8
Original line number Original line Diff line number Diff line
@@ -17,16 +17,15 @@
package com.android.systemui.statusbar.chips.call.domain.interactor
package com.android.systemui.statusbar.chips.call.domain.interactor


import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
import javax.inject.Inject
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow


/** Interactor for the ongoing phone call chip shown in the status bar. */
/** Interactor for the ongoing phone call chip shown in the status bar. */
@SysUISingleton
@SysUISingleton
open class CallChipInteractor @Inject constructor() : OngoingActivityChipInteractor {
class CallChipInteractor
    // TODO(b/332662551): Implement this flow.
@Inject
    override val chip: StateFlow<OngoingActivityChipModel> =
constructor(
        MutableStateFlow(OngoingActivityChipModel.Hidden)
    repository: OngoingCallRepository,
) {
    val ongoingCallState = repository.ongoingCallState
}
}
+91 −0
Original line number Original line 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.call.ui.viewmodel

import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.ActivityTransitionAnimator
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.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
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 ongoing phone call chip shown in the status bar. */
@SysUISingleton
open class CallChipViewModel
@Inject
constructor(
    @Application private val scope: CoroutineScope,
    interactor: CallChipInteractor,
    systemClock: SystemClock,
    private val activityStarter: ActivityStarter,
) : OngoingActivityChipViewModel {
    override val chip: StateFlow<OngoingActivityChipModel> =
        interactor.ongoingCallState
            .map { state ->
                when (state) {
                    is OngoingCallModel.NoCall -> OngoingActivityChipModel.Hidden
                    is OngoingCallModel.InCall -> {
                        // This mimics OngoingCallController#updateChip.
                        // TODO(b/332662551): Handle `state.startTimeMs = 0` correctly (see
                        // b/192379214 and
                        // OngoingCallController.CallNotificationInfo.hasValidStartTime).
                        val startTimeInElapsedRealtime =
                            state.startTimeMs - systemClock.currentTimeMillis() +
                                systemClock.elapsedRealtime()
                        OngoingActivityChipModel.Shown(
                            icon =
                                Icon.Resource(
                                    com.android.internal.R.drawable.ic_phone,
                                    contentDescription = null,
                                ),
                            startTimeMs = startTimeInElapsedRealtime,
                        ) {
                            if (state.intent != null) {
                                val backgroundView =
                                    it.requireViewById<ChipBackgroundContainer>(
                                        R.id.ongoing_activity_chip_background
                                    )
                                // TODO(b/332662551): Log the click event.
                                // This mimics OngoingCallController#updateChipClickListener.
                                activityStarter.postStartActivityDismissingKeyguard(
                                    state.intent,
                                    ActivityTransitionAnimator.Controller.fromView(
                                        backgroundView,
                                        InteractionJankMonitor
                                            .CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
                                    )
                                )
                            }
                        }
                    }
                }
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
}
+9 −8
Original line number Original line Diff line number Diff line
@@ -14,19 +14,20 @@
 * limitations under the License.
 * limitations under the License.
 */
 */


package com.android.systemui.statusbar.chips.mediaprojection.ui.view
package com.android.systemui.statusbar.chips.casttootherdevice.ui.view


import android.os.Bundle
import android.os.Bundle
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.res.R
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel.Companion.CAST_TO_OTHER_DEVICE_ICON
import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.SystemUIDialog


/** A dialog that lets the user stop an ongoing cast-screen-to-other-device event. */
/** A dialog that lets the user stop an ongoing cast-screen-to-other-device event. */
class EndCastToOtherDeviceDialogDelegate(
class EndCastToOtherDeviceDialogDelegate(
    private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
    private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
    private val interactor: MediaProjectionChipInteractor,
    private val stopAction: () -> Unit,
    private val state: MediaProjectionState.Projecting,
    private val state: ProjectionChipModel.Projecting,
) : SystemUIDialog.Delegate {
) : SystemUIDialog.Delegate {
    override fun createDialog(): SystemUIDialog {
    override fun createDialog(): SystemUIDialog {
        return endMediaProjectionDialogHelper.createDialog(this)
        return endMediaProjectionDialogHelper.createDialog(this)
@@ -34,11 +35,11 @@ class EndCastToOtherDeviceDialogDelegate(


    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
        with(dialog) {
        with(dialog) {
            setIcon(MediaProjectionChipInteractor.CAST_TO_OTHER_DEVICE_ICON)
            setIcon(CAST_TO_OTHER_DEVICE_ICON)
            setTitle(R.string.cast_to_other_device_stop_dialog_title)
            setTitle(R.string.cast_to_other_device_stop_dialog_title)
            setMessage(
            setMessage(
                endMediaProjectionDialogHelper.getDialogMessage(
                endMediaProjectionDialogHelper.getDialogMessage(
                    state,
                    state.projectionState,
                    genericMessageResId = R.string.cast_to_other_device_stop_dialog_message,
                    genericMessageResId = R.string.cast_to_other_device_stop_dialog_message,
                    specificAppMessageResId =
                    specificAppMessageResId =
                        R.string.cast_to_other_device_stop_dialog_message_specific_app,
                        R.string.cast_to_other_device_stop_dialog_message_specific_app,
@@ -48,7 +49,7 @@ class EndCastToOtherDeviceDialogDelegate(
            // button is clicked anyway.
            // button is clicked anyway.
            setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
            setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
            setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ ->
            setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ ->
                interactor.stopProjecting()
                stopAction.invoke()
            }
            }
        }
        }
    }
    }
+107 −0
Original line number Original line 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.casttootherdevice.ui.viewmodel

import androidx.annotation.DrawableRes
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.common.shared.model.ContentDescription
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.casttootherdevice.ui.view.EndCastToOtherDeviceDialogDelegate
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
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 cast-to-other-device chip, shown when sharing your phone screen content to a
 * different device. (Triggered from the Quick Settings Cast tile or from the Settings app.)
 */
@SysUISingleton
class CastToOtherDeviceChipViewModel
@Inject
constructor(
    @Application private val scope: CoroutineScope,
    private val mediaProjectionChipInteractor: MediaProjectionChipInteractor,
    private val systemClock: SystemClock,
    private val dialogTransitionAnimator: DialogTransitionAnimator,
    private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
) : OngoingActivityChipViewModel {
    override val chip: StateFlow<OngoingActivityChipModel> =
        // TODO(b/342169876): The MediaProjection APIs are not invoked for certain
        // cast-to-other-device events, like audio-only casting. We should also listen to
        // MediaRouter APIs to cover all cast events.
        mediaProjectionChipInteractor.projection
            .map { projectionModel ->
                when (projectionModel) {
                    is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Hidden
                    is ProjectionChipModel.Projecting -> {
                        if (projectionModel.type != ProjectionChipModel.Type.CAST_TO_OTHER_DEVICE) {
                            OngoingActivityChipModel.Hidden
                        } else {
                            createCastToOtherDeviceChip(projectionModel)
                        }
                    }
                }
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)

    /** Stops the currently active projection. */
    private fun stopProjecting() {
        mediaProjectionChipInteractor.stopProjecting()
    }

    private fun createCastToOtherDeviceChip(
        state: ProjectionChipModel.Projecting,
    ): OngoingActivityChipModel.Shown {
        return OngoingActivityChipModel.Shown(
            icon =
                Icon.Resource(
                    CAST_TO_OTHER_DEVICE_ICON,
                    ContentDescription.Resource(R.string.accessibility_casting),
                ),
            // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
            startTimeMs = systemClock.elapsedRealtime(),
            createDialogLaunchOnClickListener(
                createCastToOtherDeviceDialogDelegate(state),
                dialogTransitionAnimator,
            ),
        )
    }

    private fun createCastToOtherDeviceDialogDelegate(state: ProjectionChipModel.Projecting) =
        EndCastToOtherDeviceDialogDelegate(
            endMediaProjectionDialogHelper,
            stopAction = this::stopProjecting,
            state,
        )

    companion object {
        @DrawableRes val CAST_TO_OTHER_DEVICE_ICON = R.drawable.ic_cast_connected
    }
}
Loading