/* * Copyright (C) 2025 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.privatespace.filetransfer import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.content.ContextWrapper import android.icu.text.MessageFormat import android.multiuser.Flags import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.CheckFlagsRule import android.platform.test.flag.junit.DeviceFlagsValueProvider import androidx.test.platform.app.InstrumentationRegistry import com.android.bedstead.harrier.BedsteadJUnit4 import com.android.privatespace.R import com.android.privatespace.annotation.NumberOfFilesTestParameter import com.android.privatespace.annotation.TransferType import com.android.privatespace.annotation.TransferTypeTestParameter import com.google.common.truth.Truth.assertThat import java.util.HashMap import java.util.Locale import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.atMostOnce import org.mockito.Mockito.mockitoSession import org.mockito.Mockito.verify import org.mockito.MockitoSession import org.mockito.kotlin.verifyNoMoreInteractions @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MOVING_CONTENT_INTO_PRIVATE_SPACE) @RunWith(BedsteadJUnit4::class) class NotificationsHelperTest { @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() private lateinit var notificationsHelper: NotificationsHelper private lateinit var mockContext: Context private var realContext: Context = InstrumentationRegistry.getInstrumentation().targetContext @Mock private lateinit var notificationManager: NotificationManager private lateinit var mockitoSession: MockitoSession companion object { private const val CHANNEL_ID: String = "FileTransferProgress" private const val COUNT_NOTIFICATION_ARGUMENT_KEY: String = "count" } @Before fun setUp() { mockitoSession = mockitoSession().initMocks(this).startMocking() mockContext = object : ContextWrapper(realContext) { override fun getSystemService(name: String): Any? { if (NOTIFICATION_SERVICE == name) { return notificationManager } return super.getSystemService(name) } } notificationsHelper = NotificationsHelper(mockContext) } @After fun tearDown() { mockitoSession.finishMocking() } @Test fun testCreateNotificationChannel() { val notificationChannelCaptor = ArgumentCaptor.forClass(NotificationChannel::class.java) notificationsHelper.createNotificationChannel() verify(notificationManager).createNotificationChannel(notificationChannelCaptor.capture()) val notificationChannel: NotificationChannel = notificationChannelCaptor.value assertThat(notificationChannel.id).isEqualTo(CHANNEL_ID) assertThat(notificationChannel.importance).isEqualTo(NotificationManager.IMPORTANCE_HIGH) assertThat(notificationChannel.name).isNotNull() assertThat(notificationChannel.description).isNotNull() assertThat(notificationChannel.canShowBadge()).isFalse() } @Test fun testUpdateProgressNotification( @NumberOfFilesTestParameter numberOfFiles: Int, @TransferTypeTestParameter transferTypeEnum: TransferType, ) { val keepOriginal = transferTypeEnum == TransferType.COPY notificationsHelper.updateProgressNotification(progress = 23, numberOfFiles, keepOriginal) val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java) verify(notificationManager) .notify(eq(notificationsHelper.notificationId), notificationCaptor.capture()) verifyProgressNotification(notificationCaptor.value, keepOriginal, numberOfFiles) } @Test fun testNoProgressNotificationAfterCompletion() { notificationsHelper.displaySuccessfulCompletionNotification( numberOfFiles = 10, keepOriginal = true, ) notificationsHelper.updateProgressNotification( progress = 90, numberOfFiles = 10, keepOriginal = true, ) verify(notificationManager, atMostOnce()) .notify(eq(notificationsHelper.notificationId), any()) } @Test fun testDisplayCompletionNotification( @NumberOfFilesTestParameter numberOfFiles: Int, @TransferTypeTestParameter transferType: TransferType, ) { val keepOriginal = transferType == TransferType.COPY notificationsHelper.displaySuccessfulCompletionNotification(numberOfFiles, keepOriginal) val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java) verify(notificationManager) .notify(eq(notificationsHelper.notificationId), notificationCaptor.capture()) verifySuccessfulCompletionNotification( notificationCaptor.value, keepOriginal, numberOfFiles, ) } @Test fun testBuildProgressNotification( @NumberOfFilesTestParameter numberOfFiles: Int, @TransferTypeTestParameter transferTypeEnum: TransferType, ) { val keepOriginal = transferTypeEnum == TransferType.COPY val notification = notificationsHelper.buildProgressNotification( progress = 10, numberOfFiles, keepOriginal, ) verifyProgressNotification(notification, keepOriginal, numberOfFiles) } @Test fun testDisplayPartialTransferErrorNotification( @TransferTypeTestParameter transferTypeEnum: TransferType ) { val keepOriginal = transferTypeEnum == TransferType.COPY notificationsHelper.displayPartialTransferErrorNotification(keepOriginal) val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java) verify(notificationManager) .notify(eq(notificationsHelper.notificationId), notificationCaptor.capture()) verifyPartialTransferErrorNotification(notificationCaptor.value, keepOriginal) } @Test fun testNoProgressNotificationAfterPartialTransferErrorNotification() { notificationsHelper.displayPartialTransferErrorNotification(false) verify(notificationManager).notify(eq(notificationsHelper.notificationId), any()) notificationsHelper.updateProgressNotification( progress = 90, numberOfFiles = 10, keepOriginal = true, ) verifyNoMoreInteractions(notificationManager) } @Test fun testPostNotEnoughStorageNotification( @TransferTypeTestParameter transferTypeEnum: TransferType ) { val keepOriginal = transferTypeEnum == TransferType.COPY notificationsHelper.postNotEnoughStorageNotification(keepOriginal) val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java) verify(notificationManager) .notify(eq(notificationsHelper.notificationId), notificationCaptor.capture()) verifyNotEnoughStorageNotification(notificationCaptor.value, keepOriginal) } @Test fun testNoProgressNotificationAfterNotEnoughStorageNotification() { notificationsHelper.postNotEnoughStorageNotification(false) verify(notificationManager).notify(eq(notificationsHelper.notificationId), any()) notificationsHelper.updateProgressNotification( progress = 90, numberOfFiles = 10, keepOriginal = true, ) verifyNoMoreInteractions(notificationManager) } @Test fun testDisplayInterruptedTransferNotification( @TransferTypeTestParameter transferTypeEnum: TransferType ) { val keepOriginal = transferTypeEnum == TransferType.COPY notificationsHelper.displayInterruptedTransferNotification(keepOriginal) val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java) verify(notificationManager) .notify(eq(notificationsHelper.notificationId), notificationCaptor.capture()) verifyInterruptedTransferNotification(notificationCaptor.value, keepOriginal) } @Test fun testNoProgressNotificationAfterInterruptedTransferNotification() { notificationsHelper.displayInterruptedTransferNotification(false) verify(notificationManager).notify(eq(notificationsHelper.notificationId), any()) notificationsHelper.updateProgressNotification( progress = 90, numberOfFiles = 10, keepOriginal = true, ) verifyNoMoreInteractions(notificationManager) } @Test fun testDifferentNotificationHelpersHaveDifferentIds() { assertThat(notificationsHelper.notificationId) .isNotEqualTo(NotificationsHelper(mockContext).notificationId) } private fun verifySuccessfulCompletionNotification( notification: Notification, keepOriginal: Boolean, numberOfFiles: Int, ) { assertThat(notification.channelId).isEqualTo(CHANNEL_ID) comparePluralizedStringWithResources( notification.extras.getString(Notification.EXTRA_TITLE), if (keepOriginal) R.string.filetransfer_notification_copy_complete_title else R.string.filetransfer_notification_move_complete_title, quantity = numberOfFiles, ) comparePluralizedStringWithResources( notification.extras.getString(Notification.EXTRA_TEXT), if (keepOriginal) R.string.filetransfer_notification_copy_complete_text else R.string.filetransfer_notification_move_complete_text, quantity = numberOfFiles, ) assertThat(notification.actions.size).isEqualTo(1) assertThat(notification.actions.get(0).actionIntent).isNotNull() } private fun verifyProgressNotification( notification: Notification, keepOriginal: Boolean, numberOfFiles: Int, ) { assertThat(notification.channelId).isEqualTo(CHANNEL_ID) comparePluralizedStringWithResources( notification.extras.getString(Notification.EXTRA_TITLE), if (keepOriginal) R.string.filetransfer_notification_copy_progress_title else R.string.filetransfer_notification_move_progress_title, quantity = numberOfFiles, ) comparePluralizedStringWithResources( notification.extras.getString(Notification.EXTRA_TEXT), if (keepOriginal) R.string.filetransfer_notification_copy_progress_text else R.string.filetransfer_notification_move_progress_text, quantity = numberOfFiles, ) assertThat(notification.actions.size).isEqualTo(1) assertThat(notification.actions.get(0).actionIntent).isNotNull() } private fun verifyNotEnoughStorageNotification( notification: Notification, keepOriginal: Boolean, ) { assertThat(notification.channelId).isEqualTo(CHANNEL_ID) compareStringWithResources( notification.extras.getString(Notification.EXTRA_TITLE), if (keepOriginal) R.string.filetransfer_notification_copy_error_title else R.string.filetransfer_notification_move_error_title, ) compareStringWithResources( notification.extras.getString(Notification.EXTRA_TEXT), R.string.filetransfer_notification_insufficient_storage_error_message, ) assertThat(notification.actions.size).isEqualTo(1) assertThat(notification.actions.get(0).actionIntent).isNotNull() } private fun verifyInterruptedTransferNotification( notification: Notification, keepOriginal: Boolean, ) { assertThat(notification.channelId).isEqualTo(CHANNEL_ID) compareStringWithResources( notification.extras.getString(Notification.EXTRA_TITLE), if (keepOriginal) R.string.filetransfer_notification_incomplete_copy_transfer_title else R.string.filetransfer_notification_incomplete_move_transfer_title, ) compareStringWithResources( notification.extras.getString(Notification.EXTRA_TEXT), if (keepOriginal) R.string.filetransfer_notification_incomplete_copy_transfer_message else R.string.filetransfer_notification_incomplete_move_transfer_message, ) } private fun verifyPartialTransferErrorNotification( notification: Notification, keepOriginal: Boolean, ) { assertThat(notification.channelId).isEqualTo(CHANNEL_ID) compareStringWithResources( notification.extras.getString(Notification.EXTRA_TITLE), if (keepOriginal) R.string.filetransfer_notification_partial_copy_error_title else R.string.filetransfer_notification_partial_move_error_title, ) compareStringWithResources( notification.extras.getString(Notification.EXTRA_TEXT), if (keepOriginal) R.string.filetransfer_generic_copy_error_message else R.string.filetransfer_generic_move_error_message, ) assertThat(notification.actions).isNull() } private fun compareStringWithResources(actual: String?, expectedResId: Int) { assertThat(actual).isEqualTo(realContext.resources.getString(expectedResId)) } private fun comparePluralizedStringWithResources( actual: String?, expectedResId: Int, quantity: Int = 1, ) { assertThat(actual) .isEqualTo(applyFilesCount(quantity, realContext.resources.getString(expectedResId))) } private fun applyFilesCount(count: Int, msg: String): String { val msgFormat = MessageFormat(msg, Locale.getDefault()) val arguments = HashMap() arguments.put(COUNT_NOTIFICATION_ARGUMENT_KEY, count) return msgFormat.format(arguments) } }