/* * 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.Service import android.content.ContentResolver import android.content.Context import android.content.Intent import android.multiuser.Flags import android.net.Uri import android.os.Environment 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.annotation.TransferType import com.android.privatespace.annotation.TransferTypeTestParameter import com.android.privatespace.filetransfer.FileTransferService.Companion.DESTINATION_PATH_EXTRA import com.android.privatespace.filetransfer.FileTransferService.Companion.KEEP_ORIGINAL_EXTRA import com.android.privatespace.filetransfer.FileTransferService.Companion.SOURCE_URIS_EXTRA import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.inOrder import org.mockito.Mockito.spy import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.doNothing import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @RunWith(BedsteadJUnit4::class) @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MOVING_CONTENT_INTO_PRIVATE_SPACE) class FileTransferServiceTest { @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() private lateinit var fileTransferService: FileTransferService private lateinit var mockFileTransferManager: IFileTransferManager private lateinit var notificationsHelper: NotificationsHelper private lateinit var mockContext: Context private lateinit var mockContentResolver: ContentResolver private var closeable: AutoCloseable? = null private lateinit var mockFileTransferStateRepository: FileTransferStateRepository private val testScope = TestScope(UnconfinedTestDispatcher()) @Before fun setUp() { closeable = MockitoAnnotations.openMocks(this) val realContext = InstrumentationRegistry.getInstrumentation().targetContext mockContext = spy(realContext) mockContentResolver = mock() mockFileTransferManager = mock() notificationsHelper = mock() mockFileTransferStateRepository = mock() doReturn(mockContentResolver).`when`(mockContext).contentResolver fileTransferService = spy(FileTransferService(testScope)) doReturn(mockContext).`when`(fileTransferService).applicationContext doReturn(notificationsHelper).`when`(fileTransferService).initializeNotificationsHelper() doReturn(mockFileTransferManager) .`when`(fileTransferService) .initializeFileTransferManager(notificationsHelper) doReturn(mockFileTransferStateRepository) .`when`(fileTransferService) .initializeFileTransferStateRepository() doReturn(mockContext).`when`(fileTransferService).getApplicationContext() doNothing().`when`(fileTransferService).startForeground(any(), any()) doNothing().`when`(fileTransferService).stopForeground(any()) doNothing().`when`(fileTransferService).stopSelf() fileTransferService.onCreate() } @After fun tearDown() { closeable?.close() } @Test fun onStartCommand_withNullUris_doesNotStartTransfer() = runTest { val intent = prepareMockIntent() intent.putParcelableArrayListExtra(SOURCE_URIS_EXTRA, null) val result = fileTransferService.onStartCommand(intent, flags = 0, startId = 1) assertEquals(Service.START_NOT_STICKY, result) verify(mockFileTransferManager, never()).transferFiles(any(), any(), any()) } @Test fun onStartCommand_withEmptyUris_doesNotStartTransfer() = runTest { val intent = prepareMockIntent() intent.putParcelableArrayListExtra(SOURCE_URIS_EXTRA, arrayListOf()) val result = fileTransferService.onStartCommand(intent, flags = 0, startId = 1) assertEquals(Service.START_NOT_STICKY, result) verify(mockFileTransferManager, never()).transferFiles(any(), any(), any()) } @Test fun onStartCommand_withValidUris_startsTransferAndStopsService( @TransferTypeTestParameter transferType: TransferType ) = testScope.runTest { val keepOriginal = transferType == TransferType.COPY val mockUri = mock() val uris = arrayListOf(mockUri) val mockIntent = prepareMockIntent() mockIntent.putParcelableArrayListExtra(SOURCE_URIS_EXTRA, uris) mockIntent.putExtra(KEEP_ORIGINAL_EXTRA, keepOriginal) val notification = mock() whenever(notificationsHelper.buildProgressNotification(any(), any(), any())) .thenReturn(notification) val result = fileTransferService.onStartCommand(mockIntent, flags = 0, startId = 1) assertEquals(Service.START_NOT_STICKY, result) val inOrder = inOrder( fileTransferService, mockFileTransferManager, mockFileTransferStateRepository, notificationsHelper, ) inOrder.verify(notificationsHelper).createNotificationChannel() inOrder.verify(fileTransferService).startForeground(any(), any()) inOrder.verify(mockFileTransferManager).transferFiles(eq(uris), eq(keepOriginal), any()) inOrder .verify(mockFileTransferStateRepository) .saveState(any(), eq(keepOriginal), eq(false)) inOrder.verify(fileTransferService).stopForeground(Service.STOP_FOREGROUND_REMOVE) inOrder.verify(fileTransferService).stopSelf() inOrder .verify(mockFileTransferManager) .postEndOfTransferNotification(eq(uris.size), eq(keepOriginal)) } @Test fun onDestroy_releasesUriPermissions(@TransferTypeTestParameter transferType: TransferType) { val keepOriginal = transferType == TransferType.COPY val intent = prepareMockIntent() val mockUri = mock() val testUris = arrayListOf(mockUri) intent.putParcelableArrayListExtra(SOURCE_URIS_EXTRA, testUris) intent.putExtra(KEEP_ORIGINAL_EXTRA, keepOriginal) fileTransferService.onStartCommand(intent, flags = 0, startId = 1) fileTransferService.onDestroy() val verificationMode = if (!keepOriginal) never() else times(1) testUris.forEach { uri -> verify(mockContentResolver, verificationMode) .releasePersistableUriPermission( eq(uri), eq( Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ), ) } } private fun prepareMockIntent(): Intent { val intent = Intent(mockContext, FileTransferService::class.java) intent.putExtra(DESTINATION_PATH_EXTRA, Environment.DIRECTORY_DOWNLOADS) return intent } }