• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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