/* * 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 import android.app.ActivityOptions import android.content.ActivityNotFoundException import android.content.Intent import android.content.IntentSender.SendIntentException import android.content.pm.LauncherApps import android.net.Uri import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.material3.Button import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource /** * The main activity for the Private Space system app. * * This activity handles the following actions: * - `com.android.privatespace.action.OPEN_MARKET_APP`: Opens the market app (e.g. Play Store) to * allow the user to install apps in the Private Space. * - `com.android.privatespace.action.ADD_FILES`: Opens a document picker to select files to add to * the Private Space. When the document picker intent returns the dialog is shown to the user to * choose between moving the files to the Private Space, copying them, or canceling the operation. */ class PrivateSpaceActivity : ComponentActivity() { private lateinit var documentPickerLauncher: ActivityResultLauncher private val viewModel: PrivateSpaceViewModel by viewModels() companion object { private const val TAG = "PrivateSpaceActivity" private const val ACTION_ADD_FILES = "com.android.privatespace.action.ADD_FILES" private const val ACTION_OPEN_MARKET_APP = "com.android.privatespace.action.OPEN_MARKET_APP" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) documentPickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { result.data?.let { handleDocumentSelection(it) } } else { Log.w( TAG, "Document picker activity result not OK: resultCode = ${result.resultCode}", ) viewModel.finishFlow() } } setContent { PrivateSpaceAppTheme { PrivateSpaceActivityScreen(viewModel = viewModel) } } handleIntent(intent) } private fun handleIntent(intent: Intent) { Log.d(TAG, "handleIntent action: ${intent.action}") when (intent.action) { ACTION_ADD_FILES -> openDocumentPicker() ACTION_OPEN_MARKET_APP -> openMarketApp() else -> { Log.d(TAG, "No known action specified") // TODO(b/397383858): default to the Private Space settings screen viewModel.finishFlow() } } } private fun openDocumentPicker() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { type = "*/*" addCategory(Intent.CATEGORY_OPENABLE) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) } documentPickerLauncher.launch(intent) } private fun openMarketApp() { val launcherApps = applicationContext.getSystemService(LauncherApps::class.java) ?: run { Log.e(TAG, "Failed to get LauncherApps service") viewModel.finishFlow() return } try { val intentSender = launcherApps.getAppMarketActivityIntent( applicationContext.packageName, applicationContext.user, ) intentSender?.let { // Satisfy BAL restrictions. val fillInIntent = Intent() val options = ActivityOptions.makeBasic() .setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE ) .toBundle() startIntentSender(it, fillInIntent, 0, 0, 0, options) } ?: run { Log.e(TAG, "Failed to open market app.") } } catch (e: Exception) { when (e) { is NullPointerException, is ActivityNotFoundException, is SecurityException, is SendIntentException -> { Log.e(TAG, "Private Space could not start the market app", e) } else -> throw e } } finally { viewModel.finishFlow() } } private fun handleDocumentSelection(data: Intent) { val uris = buildList { // Single URI is passed in data, multiple URIs are passed in clipData data.data?.let { persistUriPermissions(it) add(it) } data.clipData?.let { clipData -> for (i in 0 until clipData.itemCount) { val uri: Uri = clipData.getItemAt(i).uri persistUriPermissions(uri) add(uri) } } } viewModel.showMoveFilesDialog(uris) } @Composable fun PrivateSpaceActivityScreen(viewModel: PrivateSpaceViewModel) { when (viewModel.uiState) { PrivateSpaceUiState.STARTED -> { // Show nothing, wait for the results of the document picker. } PrivateSpaceUiState.SHOW_MOVE_FILES_DIALOG -> { ThreeButtonAlertDialog( onDismissRequest = viewModel::finishFlow, title = stringResource(R.string.move_files_dialog_title), message = stringResource(R.string.move_files_dialog_summary), primaryButton = { Button(onClick = { viewModel.moveFiles(applicationContext) }) { Text(stringResource(R.string.move_files_dialog_button_label_move)) } }, secondaryButton = { OutlinedButton(onClick = { viewModel.copyFiles(applicationContext) }) { Text(stringResource(R.string.move_files_dialog_button_label_copy)) } }, dismissButton = { TextButton(onClick = viewModel::finishFlow) { Text(stringResource(R.string.move_files_dialog_button_label_cancel)) } }, ) } PrivateSpaceUiState.FINISHED -> { finish() } } } private fun persistUriPermissions(uri: Uri) { applicationContext.contentResolver.takePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION, ) } }