1 /* <lambda>null2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.privatespace 17 18 import android.app.ActivityOptions 19 import android.content.ActivityNotFoundException 20 import android.content.Intent 21 import android.content.IntentSender.SendIntentException 22 import android.content.pm.LauncherApps 23 import android.net.Uri 24 import android.os.Bundle 25 import android.util.Log 26 import androidx.activity.ComponentActivity 27 import androidx.activity.compose.setContent 28 import androidx.activity.result.ActivityResultLauncher 29 import androidx.activity.result.contract.ActivityResultContracts 30 import androidx.activity.viewModels 31 import androidx.compose.material3.Button 32 import androidx.compose.material3.OutlinedButton 33 import androidx.compose.material3.Text 34 import androidx.compose.material3.TextButton 35 import androidx.compose.runtime.Composable 36 import androidx.compose.ui.res.stringResource 37 38 /** 39 * The main activity for the Private Space system app. 40 * 41 * This activity handles the following actions: 42 * - `com.android.privatespace.action.OPEN_MARKET_APP`: Opens the market app (e.g. Play Store) to 43 * allow the user to install apps in the Private Space. 44 * - `com.android.privatespace.action.ADD_FILES`: Opens a document picker to select files to add to 45 * the Private Space. When the document picker intent returns the dialog is shown to the user to 46 * choose between moving the files to the Private Space, copying them, or canceling the operation. 47 */ 48 class PrivateSpaceActivity : ComponentActivity() { 49 50 private lateinit var documentPickerLauncher: ActivityResultLauncher<Intent> 51 52 private val viewModel: PrivateSpaceViewModel by viewModels() 53 54 companion object { 55 private const val TAG = "PrivateSpaceActivity" 56 private const val ACTION_ADD_FILES = "com.android.privatespace.action.ADD_FILES" 57 private const val ACTION_OPEN_MARKET_APP = "com.android.privatespace.action.OPEN_MARKET_APP" 58 } 59 60 override fun onCreate(savedInstanceState: Bundle?) { 61 super.onCreate(savedInstanceState) 62 63 documentPickerLauncher = 64 registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 65 if (result.resultCode == RESULT_OK) { 66 result.data?.let { handleDocumentSelection(it) } 67 } else { 68 Log.w( 69 TAG, 70 "Document picker activity result not OK: resultCode = ${result.resultCode}", 71 ) 72 viewModel.finishFlow() 73 } 74 } 75 76 setContent { PrivateSpaceAppTheme { PrivateSpaceActivityScreen(viewModel = viewModel) } } 77 handleIntent(intent) 78 } 79 80 private fun handleIntent(intent: Intent) { 81 Log.d(TAG, "handleIntent action: ${intent.action}") 82 83 when (intent.action) { 84 ACTION_ADD_FILES -> openDocumentPicker() 85 ACTION_OPEN_MARKET_APP -> openMarketApp() 86 else -> { 87 Log.d(TAG, "No known action specified") 88 // TODO(b/397383858): default to the Private Space settings screen 89 viewModel.finishFlow() 90 } 91 } 92 } 93 94 private fun openDocumentPicker() { 95 val intent = 96 Intent(Intent.ACTION_OPEN_DOCUMENT).apply { 97 type = "*/*" 98 addCategory(Intent.CATEGORY_OPENABLE) 99 putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) 100 } 101 documentPickerLauncher.launch(intent) 102 } 103 104 private fun openMarketApp() { 105 val launcherApps = 106 applicationContext.getSystemService(LauncherApps::class.java) 107 ?: run { 108 Log.e(TAG, "Failed to get LauncherApps service") 109 viewModel.finishFlow() 110 return 111 } 112 113 try { 114 val intentSender = 115 launcherApps.getAppMarketActivityIntent( 116 applicationContext.packageName, 117 applicationContext.user, 118 ) 119 intentSender?.let { 120 // Satisfy BAL restrictions. 121 val fillInIntent = Intent() 122 val options = 123 ActivityOptions.makeBasic() 124 .setPendingIntentBackgroundActivityStartMode( 125 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE 126 ) 127 .toBundle() 128 startIntentSender(it, fillInIntent, 0, 0, 0, options) 129 } ?: run { Log.e(TAG, "Failed to open market app.") } 130 } catch (e: Exception) { 131 when (e) { 132 is NullPointerException, 133 is ActivityNotFoundException, 134 is SecurityException, 135 is SendIntentException -> { 136 Log.e(TAG, "Private Space could not start the market app", e) 137 } 138 else -> throw e 139 } 140 } finally { 141 viewModel.finishFlow() 142 } 143 } 144 145 private fun handleDocumentSelection(data: Intent) { 146 val uris = buildList { 147 // Single URI is passed in data, multiple URIs are passed in clipData 148 data.data?.let { 149 persistUriPermissions(it) 150 add(it) 151 } 152 data.clipData?.let { clipData -> 153 for (i in 0 until clipData.itemCount) { 154 val uri: Uri = clipData.getItemAt(i).uri 155 persistUriPermissions(uri) 156 add(uri) 157 } 158 } 159 } 160 161 viewModel.showMoveFilesDialog(uris) 162 } 163 164 @Composable 165 fun PrivateSpaceActivityScreen(viewModel: PrivateSpaceViewModel) { 166 when (viewModel.uiState) { 167 PrivateSpaceUiState.STARTED -> { 168 // Show nothing, wait for the results of the document picker. 169 } 170 PrivateSpaceUiState.SHOW_MOVE_FILES_DIALOG -> { 171 ThreeButtonAlertDialog( 172 onDismissRequest = viewModel::finishFlow, 173 title = stringResource(R.string.move_files_dialog_title), 174 message = stringResource(R.string.move_files_dialog_summary), 175 primaryButton = { 176 Button(onClick = { viewModel.moveFiles(applicationContext) }) { 177 Text(stringResource(R.string.move_files_dialog_button_label_move)) 178 } 179 }, 180 secondaryButton = { 181 OutlinedButton(onClick = { viewModel.copyFiles(applicationContext) }) { 182 Text(stringResource(R.string.move_files_dialog_button_label_copy)) 183 } 184 }, 185 dismissButton = { 186 TextButton(onClick = viewModel::finishFlow) { 187 Text(stringResource(R.string.move_files_dialog_button_label_cancel)) 188 } 189 }, 190 ) 191 } 192 PrivateSpaceUiState.FINISHED -> { 193 finish() 194 } 195 } 196 } 197 198 private fun persistUriPermissions(uri: Uri) { 199 applicationContext.contentResolver.takePersistableUriPermission( 200 uri, 201 Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION, 202 ) 203 } 204 } 205