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

Commit 9fe5d9f0 authored by yyalan's avatar yyalan
Browse files

[Partial Screen Sharing] Task Switcher Notification

Show or cancel notification when a task switch is detected

Bug: 286201515
Test: TaskSwitcherNotificationCoordinatorTest.kt
Change-Id: Iee35ebcf968cd54fce5d5f345991bb82a1e8b964
parent 482c676e
Loading
Loading
Loading
Loading
+10 −0
Original line number Original line Diff line number Diff line
@@ -1107,6 +1107,16 @@
    <!-- System sharing media projection permission button to continue. [CHAR LIMIT=60] -->
    <!-- System sharing media projection permission button to continue. [CHAR LIMIT=60] -->
    <string name="media_projection_entry_generic_permission_dialog_continue">Start</string>
    <string name="media_projection_entry_generic_permission_dialog_continue">Start</string>


    <!-- Task switcher notification -->
    <!-- Task switcher notification text. [CHAR LIMIT=100] -->
    <string name="media_projection_task_switcher_text">Sharing pauses when you switch apps</string>
    <!-- The action for switching to the foreground task. [CHAR LIMIT=40] -->
    <string name="media_projection_task_switcher_action_switch">Share this app instead</string>
    <!-- The action for switching back to the projected task. [CHAR LIMIT=40] -->
    <string name="media_projection_task_switcher_action_back">Switch back</string>
    <!-- Task switcher notification channel name. [CHAR LIMIT=40] -->
    <string name="media_projection_task_switcher_notification_channel">App switch</string>

    <!-- Title for the dialog that is shown when screen capturing is disabled by enterprise policy. [CHAR LIMIT=100] -->
    <!-- Title for the dialog that is shown when screen capturing is disabled by enterprise policy. [CHAR LIMIT=100] -->
    <string name="screen_capturing_disabled_by_policy_dialog_title">Blocked by your IT admin</string>
    <string name="screen_capturing_disabled_by_policy_dialog_title">Blocked by your IT admin</string>


+49 −14
Original line number Original line Diff line number Diff line
@@ -16,15 +16,19 @@


package com.android.systemui.mediaprojection.taskswitcher.ui
package com.android.systemui.mediaprojection.taskswitcher.ui


import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Context
import android.util.Log
import android.util.Log
import android.widget.Toast
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.NotShowing
import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.NotShowing
import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.Showing
import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.Showing
import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
import com.android.systemui.util.NotificationChannels
import javax.inject.Inject
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
@@ -37,38 +41,69 @@ class TaskSwitcherNotificationCoordinator
@Inject
@Inject
constructor(
constructor(
    private val context: Context,
    private val context: Context,
    private val notificationManager: NotificationManager,
    @Application private val applicationScope: CoroutineScope,
    @Application private val applicationScope: CoroutineScope,
    @Main private val mainDispatcher: CoroutineDispatcher,
    @Main private val mainDispatcher: CoroutineDispatcher,
    private val viewModel: TaskSwitcherNotificationViewModel,
    private val viewModel: TaskSwitcherNotificationViewModel,
) {
) {

    fun start() {
    fun start() {
        applicationScope.launch {
        applicationScope.launch {
            viewModel.uiState.flowOn(mainDispatcher).collect { uiState ->
            viewModel.uiState.flowOn(mainDispatcher).collect { uiState ->
                Log.d(TAG, "uiState -> $uiState")
                Log.d(TAG, "uiState -> $uiState")
                when (uiState) {
                when (uiState) {
                    is Showing -> showNotification(uiState)
                    is Showing -> showNotification()
                    is NotShowing -> hideNotification()
                    is NotShowing -> hideNotification()
                }
                }
            }
            }
        }
        }
    }
    }


    private fun showNotification(uiState: Showing) {
    private fun showNotification() {
        val text =
        notificationManager.notify(TAG, NOTIFICATION_ID, createNotification())
            """
    }
            Sharing pauses when you switch apps.

            Share this app instead.
    private fun createNotification(): Notification {
            Switch back.
        // TODO(b/286201261): implement actions
            """
        val actionSwitch =
                .trimIndent()
            Notification.Action.Builder(
        // TODO(b/286201515): Create actual notification.
                    /* icon = */ null,
        Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
                    context.getString(R.string.media_projection_task_switcher_action_switch),
                    /* intent = */ null
                )
                .build()

        val actionBack =
            Notification.Action.Builder(
                    /* icon = */ null,
                    context.getString(R.string.media_projection_task_switcher_action_back),
                    /* intent = */ null
                )
                .build()

        val channel =
            NotificationChannel(
                NotificationChannels.HINTS,
                context.getString(R.string.media_projection_task_switcher_notification_channel),
                NotificationManager.IMPORTANCE_HIGH
            )
        notificationManager.createNotificationChannel(channel)
        return Notification.Builder(context, channel.id)
            .setSmallIcon(R.drawable.qs_screen_record_icon_on)
            .setAutoCancel(true)
            .setContentText(context.getString(R.string.media_projection_task_switcher_text))
            .addAction(actionSwitch)
            .addAction(actionBack)
            .setPriority(Notification.PRIORITY_HIGH)
            .setDefaults(Notification.DEFAULT_VIBRATE)
            .build()
    }
    }


    private fun hideNotification() {}
    private fun hideNotification() {
        notificationManager.cancel(NOTIFICATION_ID)
    }


    companion object {
    companion object {
        private const val TAG = "TaskSwitchNotifCoord"
        private const val TAG = "TaskSwitchNotifCoord"
        private const val NOTIFICATION_ID = 5566
    }
    }
}
}
+128 −0
Original line number Original line 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.mediaprojection.taskswitcher.ui

import android.app.Notification
import android.app.NotificationManager
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager
import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionRepository
import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mockito.verify

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidTestingRunner::class)
@SmallTest
class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() {

    private val notificationManager: NotificationManager = mock()

    private val dispatcher = UnconfinedTestDispatcher()
    private val testScope = TestScope(dispatcher)
    private val fakeActivityTaskManager = FakeActivityTaskManager()
    private val mediaRepo = FakeMediaProjectionRepository()
    private val tasksRepo =
        ActivityTaskManagerTasksRepository(
            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
            applicationScope = testScope.backgroundScope,
            backgroundDispatcher = dispatcher
        )
    private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
    private val viewModel = TaskSwitcherNotificationViewModel(interactor)

    private val coordinator =
        TaskSwitcherNotificationCoordinator(
            context,
            notificationManager,
            testScope.backgroundScope,
            dispatcher,
            viewModel
        )

    @Before
    fun setup() {
        coordinator.start()
    }

    @Test
    fun showNotification() {
        testScope.runTest {
            switchTask()

            val notification = ArgumentCaptor.forClass(Notification::class.java)
            verify(notificationManager).notify(any(), any(), notification.capture())
            assertNotification(notification)
        }
    }

    @Test
    fun hideNotification() {
        testScope.runTest {
            mediaRepo.stopProjecting()

            verify(notificationManager).cancel(any())
        }
    }

    @Test
    fun notificationIdIsConsistent() {
        testScope.runTest {
            mediaRepo.stopProjecting()
            val idCancel = argumentCaptor<Int>()
            verify(notificationManager).cancel(idCancel.capture())

            switchTask()
            val idNotify = argumentCaptor<Int>()
            verify(notificationManager).notify(any(), idNotify.capture(), any())

            assertEquals(idCancel.value, idNotify.value)
        }
    }

    private fun switchTask() {
        val projectedTask = FakeActivityTaskManager.createTask(taskId = 1)
        val foregroundTask = FakeActivityTaskManager.createTask(taskId = 2)
        mediaRepo.switchProjectedTask(projectedTask)
        fakeActivityTaskManager.moveTaskToForeground(foregroundTask)
    }

    private fun assertNotification(notification: ArgumentCaptor<Notification>) {
        val text = notification.value.extras.getCharSequence(Notification.EXTRA_TEXT)
        assertEquals(context.getString(R.string.media_projection_task_switcher_text), text)

        val actions = notification.value.actions
        assertThat(actions).hasLength(2)
    }
}