Merge "[Partial Screen Sharing] Task Switcher Notification" into udc-qpr-dev am: 929cb5c3a3

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23917785

Change-Id: I096727c139c04d830673a0944768d00b10432bf3
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Yalan Yiue
2023-07-12 09:57:54 +00:00
committed by Automerger Merge Worker
3 changed files with 187 additions and 14 deletions

View File

@@ -1114,6 +1114,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>

View File

@@ -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.
Switch back.
"""
.trimIndent()
// TODO(b/286201515): Create actual notification.
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
} }
private fun hideNotification() {} private fun createNotification(): Notification {
// TODO(b/286201261): implement actions
val actionSwitch =
Notification.Action.Builder(
/* icon = */ null,
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() {
notificationManager.cancel(NOTIFICATION_ID)
}
companion object { companion object {
private const val TAG = "TaskSwitchNotifCoord" private const val TAG = "TaskSwitchNotifCoord"
private const val NOTIFICATION_ID = 5566
} }
} }

View File

@@ -0,0 +1,128 @@
/*
* 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)
}
}