/* * 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 import android.content.IntentSender.SendIntentException import android.content.pm.LauncherApps import android.os.Bundle import android.provider.DocumentsContract import android.util.Log import android.widget.Toast 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.annotation.VisibleForTesting import androidx.lifecycle.lifecycleScope import java.util.function.Supplier import kotlinx.coroutines.launch /** * 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 @VisibleForTesting internal val viewModel: PrivateSpaceViewModel by viewModels() companion object { private const val TAG = "PrivateSpaceActivity" @VisibleForTesting internal const val ACTION_ADD_FILES = "com.android.privatespace.action.ADD_FILES" @VisibleForTesting internal const val ACTION_OPEN_MARKET_APP = "com.android.privatespace.action.OPEN_MARKET_APP" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (PrivateSpaceActivityDisabler.disableActivityIfNotPrivateProfile(applicationContext)) { return } documentPickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> viewModel.onDocumentSelectionFromDocumentPicker( result, applicationContext.contentResolver, ) } lifecycleScope.launch { viewModel.uiEventFlow.collect { event -> when (event) { PrivateSpaceUIEvent.SHOW_CANNOT_TRANSFER_FROM_SAME_USER_TOAST -> showCannotTransferFromSameUserToast() } } } setContent { PrivateSpaceAppTheme { PrivateSpaceActivityScreen( viewModel = viewModel, context = applicationContext, onOpenDocumentsPicker = this::openDocumentPicker, onFinished = this::finish, ) } } if (savedInstanceState == null) { handleIntent(intent) } } private fun handleIntent(intent: Intent) { Log.d(TAG, "handleIntent action: ${intent.action}") when (intent.action) { ACTION_ADD_FILES -> { PrivateSpaceStatsLog.write( PrivateSpaceStatsLog.PRIVATE_SPACE_ADD_BUTTON_EVENT, PrivateSpaceStatsLog .PRIVATE_SPACE_ADD_BUTTON_EVENT__ADD_BUTTON_CLICK__ADD_BUTTON_CLICK_MOVE_FILES_SHORTCUT, ) viewModel.checkIfNewTransferCanStart(applicationContext) } ACTION_OPEN_MARKET_APP -> { PrivateSpaceStatsLog.write( PrivateSpaceStatsLog.PRIVATE_SPACE_ADD_BUTTON_EVENT, PrivateSpaceStatsLog .PRIVATE_SPACE_ADD_BUTTON_EVENT__ADD_BUTTON_CLICK__ADD_BUTTON_CLICK_OPEN_MARKET_APP_SHORTCUT, ) openMarketApp() } else -> openPrivateSpaceSettings() } } private fun openDocumentPicker() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { type = "*/*" addCategory(Intent.CATEGORY_OPENABLE) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) putExtra(Intent.EXTRA_LOCAL_ONLY, true) putExtra( DocumentsContract.EXTRA_EXCLUDED_USERS, arrayListOf(applicationContext.user), ) } documentPickerLauncher.launch(intent) } private fun openMarketApp() { handleLauncherAppsIntentSender { getLauncherApps() ?.getAppMarketActivityIntent( applicationContext.packageName, applicationContext.user, ) } } private fun openPrivateSpaceSettings() { handleLauncherAppsIntentSender { getLauncherApps()?.privateSpaceSettingsIntent } viewModel.finishFlow() } private fun getLauncherApps(): LauncherApps? { return (applicationContext.getSystemService(LauncherApps::class.java) ?: run { Log.e(TAG, "Failed to get LauncherApps service") viewModel.finishFlow() null }) } private fun launchIntentSenderWithBal(intentSender: IntentSender) { val fillInIntent = Intent() val options = ActivityOptions.makeBasic() .setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE ) .toBundle() startIntentSender(intentSender, fillInIntent, 0, 0, 0, options) } private fun handleLauncherAppsIntentSender(intentSenderSupplier: Supplier) { val intentSender = intentSenderSupplier.get() intentSender?.let { try { launchIntentSenderWithBal(it) } catch (e: Exception) { when (e) { is NullPointerException, is ActivityNotFoundException, is SecurityException, is SendIntentException -> { Log.e(TAG, "Private Space could not start the intended activity", e) } else -> throw e } } finally { viewModel.finishFlow() } } ?: run { Log.e(TAG, "Failed to get IntentSender") } } private fun showCannotTransferFromSameUserToast() { Toast.makeText( applicationContext, applicationContext.resources.getString( R.string.filetransfer_error_toast_cannot_transfer_from_same_user ), Toast.LENGTH_SHORT, ) .show() } }