/* * 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.Service import android.content.Intent import android.net.Uri import android.os.Environment import android.os.IBinder import android.util.Log import androidx.annotation.OpenForTesting import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch /** * An object responsible for starting a foreground service to copy/move the user selected files into * private space. */ @OpenForTesting open class FileTransferService( private val serviceScope: CoroutineScope = CoroutineScope(Dispatchers.IO) ) : Service() { companion object { /** * A boolean extra noting whether this transfer is a copy(true) or a move(false) operation. */ const val KEEP_ORIGINAL_EXTRA: String = "keep_original_extra" /** An array extra to get the uris of the file to transfer. */ const val SOURCE_URIS_EXTRA: String = "source_uris_extra" /** * Target relative path where the given file(s) should be saved. This path should be inline * with guidelines for {@link MediaStore.MediaColumns.RELATIVE_PATH} */ const val DESTINATION_PATH_EXTRA: String = "destination_path" private var keepOriginal: Boolean = true private const val TAG: String = "FileTransferService" } private lateinit var notificationsHelper: NotificationsHelper private lateinit var fileTransferManager: IFileTransferManager private lateinit var fileTransferStateRepository: FileTransferStateRepository private var uris: List? = null override fun onCreate() { super.onCreate() notificationsHelper = initializeNotificationsHelper() fileTransferStateRepository = initializeFileTransferStateRepository() fileTransferManager = initializeFileTransferManager(notificationsHelper) } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { val uris: ArrayList? = intent.getParcelableArrayListExtra(SOURCE_URIS_EXTRA, Uri::class.java) keepOriginal = intent.getBooleanExtra(KEEP_ORIGINAL_EXTRA, true) val destinationPath = intent.getStringExtra(DESTINATION_PATH_EXTRA) ?: Environment.DIRECTORY_DOWNLOADS this.uris = uris uris?.let { val numberOfFiles = it.size if (numberOfFiles == 0) { Log.e(TAG, "No files to transfer") return@let } notificationsHelper.createNotificationChannel() serviceScope.launch { startForeground( notificationsHelper.notificationId, notificationsHelper.buildProgressNotification(0, numberOfFiles, keepOriginal), ) fileTransferManager.transferFiles(it, keepOriginal, destinationPath) fileTransferStateRepository.saveState( applicationContext, keepOriginal, transferInProgress = false, ) stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() fileTransferManager.postEndOfTransferNotification(numberOfFiles, keepOriginal) } } ?: Log.e(TAG, "Unable to parse uris from the intent: $intent") return START_NOT_STICKY } override fun onBind(intent: Intent?): IBinder? { return null } override fun onDestroy() { if (keepOriginal) { uris?.forEach { uri -> try { applicationContext.contentResolver.releasePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION, ) } catch (se: SecurityException) { Log.e(TAG, "Failed to release persisted Uri grant", se) } } } } @OpenForTesting open fun initializeNotificationsHelper(): NotificationsHelper { return NotificationsHelper(applicationContext) } @OpenForTesting open fun initializeFileTransferStateRepository(): FileTransferStateRepository { return FileTransferStateRepository } @OpenForTesting open fun initializeFileTransferManager( notificationsHelper: NotificationsHelper ): IFileTransferManager { return FileTransferManagerImpl( notificationsHelper, applicationContext, FileTransferStateRepository, ) } }