Skip to content

Commit 0cedaa8

Browse files
committed
Add Storage Access Framework sample
1 parent db6dc1d commit 0cedaa8

File tree

23 files changed

+894
-83
lines changed

23 files changed

+894
-83
lines changed

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Browse the samples inside each topic samples folder:
2121
- [User-interface](https://github.com/android/platform-samples/tree/main/samples/user-interface)
2222
- More to come...
2323

24-
We regularly add new samples to this repository. You can find a list of all the available samples [here](https://github.com/android/platform-samples/tree/main/samples).
24+
We are constantly adding new samples to this repository. You can find a list of all the available samples [here](https://github.com/android/platform-samples/tree/main/samples).
2525

2626
## How to run
2727

Diff for: app/src/main/java/com/example/platform/app/SampleDemo.kt

+9
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import com.example.platform.shared.MinSdkBox
6363
import com.example.platform.storage.mediastore.MediaStoreQuerySample
6464
import com.example.platform.storage.mediastore.SelectedPhotosAccessSample
6565
import com.example.platform.storage.photopicker.PhotoPickerSample
66+
import com.example.platform.storage.storageaccessframework.GetContentSample
6667
import com.example.platform.ui.appwidgets.AppWidgets
6768
import com.example.platform.ui.constraintlayout.AdvancedArrangementFragment
6869
import com.example.platform.ui.constraintlayout.AdvancedChainsFragment
@@ -566,6 +567,14 @@ val SAMPLE_DEMOS by lazy {
566567
apiSurface = StorageApiSurface,
567568
content = { MediaStoreQuerySample() },
568569
),
570+
ComposableSampleDemo(
571+
id = "storageaccessframework-getcontent",
572+
name = "Storage Access Framework - GET_CONTENT",
573+
description = "Open a document using the Storage Access Framework",
574+
documentation = "https://developer.android.com/training/data-storage/shared/documents-files#open-file",
575+
apiSurface = StorageApiSurface,
576+
content = { GetContentSample() },
577+
),
569578
ComposableSampleDemo(
570579
id = "selected-photos-access",
571580
name = "Selected Photos Access",

Diff for: samples/accessibility/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ android {
4242
dependencies {
4343
implementation(platform(libs.androidx.compose.bom))
4444
implementation(libs.androidx.ui)
45+
implementation(libs.androidx.ui.tooling)
4546
implementation(libs.androidx.ui.tooling.preview)
4647
implementation(libs.androidx.material3)
4748
implementation(libs.androidx.fragment)

Diff for: samples/privacy/transparency/src/main/java/com/example/platform/privacy/transparency/DataAccess.kt

-10
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,10 @@ import androidx.compose.ui.platform.LocalContext
4747
import androidx.compose.ui.platform.LocalLifecycleOwner
4848
import androidx.compose.ui.unit.dp
4949
import androidx.core.app.AppOpsManagerCompat
50-
//import androidx.core.content.getSystemService
5150
import androidx.lifecycle.Lifecycle
5251
import androidx.lifecycle.LifecycleEventObserver
5352
import androidx.lifecycle.LifecycleOwner
54-
//import com.example.platform.base.PermissionBox
5553
import com.example.platform.shared.PermissionBox
56-
//import com.google.android.catalog.framework.annotations.Sample
5754
import com.google.android.gms.location.LocationServices
5855
import com.google.android.gms.location.Priority
5956
import com.google.android.gms.tasks.CancellationTokenSource
@@ -62,13 +59,6 @@ import kotlinx.coroutines.launch
6259
import kotlinx.coroutines.tasks.await
6360

6461
@SuppressLint("MissingPermission")
65-
//@Sample(
66-
// name = "Data Access",
67-
// description = "Demonstrates how to implement data access auditing for your app to identify " +
68-
// "unexpected data access, even from third-party SDKs and libraries.",
69-
// documentation = "https://developer.android.com/guide/topics/data/audit-access",
70-
//)
71-
7262
@RequiresApi(Build.VERSION_CODES.R)
7363
@Composable
7464
fun DataAccessSample() {

Diff for: samples/privacy/transparency/src/main/java/com/example/platform/privacy/transparency/ScreenshotDetection.kt

-4
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ import androidx.fragment.app.Fragment
3939
import java.text.DateFormat
4040
import java.util.Date
4141

42-
//@Sample(
43-
// name = "Screenshot Detection",
44-
// description = "This sample shows how to detect that the user capture the screen in Android 14 onwards",
45-
//)
4642
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
4743
class ScreenshotDetectionSample : Fragment() {
4844

Diff for: samples/storage/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies {
4343
implementation(platform(libs.androidx.compose.bom))
4444
implementation(libs.androidx.ui)
4545
implementation(libs.androidx.ui.graphics)
46+
implementation(libs.androidx.ui.tooling)
4647
implementation(libs.androidx.ui.tooling.preview)
4748
implementation(libs.androidx.material3)
4849

Diff for: samples/storage/src/main/java/com/example/platform/storage/StorageSampleActivity.kt

-43
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
package com.example.platform.storage.fileprovider
2+

Diff for: samples/storage/src/main/java/com/example/platform/storage/mediastore/MediaStoreQuery.kt

-7
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,10 @@ import androidx.compose.ui.platform.LocalContext
4545
import androidx.compose.ui.text.style.TextOverflow
4646
import androidx.compose.ui.unit.dp
4747
import coil.compose.AsyncImage
48-
//import com.example.platform.base.PermissionBox
4948
import com.example.platform.shared.PermissionBox
50-
//import com.google.android.catalog.framework.annotations.Sample
5149
import kotlinx.coroutines.Dispatchers
5250
import kotlinx.coroutines.withContext
5351

54-
//@Sample(
55-
// name = "MediaStore - Query",
56-
// description = "Query files indexed by MediaStore",
57-
// documentation = "https://developer.android.com/training/data-storage/shared/media#media_store",
58-
//)
5952
@SuppressLint("MissingPermission")
6053
@Composable
6154
fun MediaStoreQuerySample() {

Diff for: samples/storage/src/main/java/com/example/platform/storage/mediastore/SelectedPhotosAccess.kt

-6
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,6 @@ import kotlinx.coroutines.Dispatchers
4848
import kotlinx.coroutines.launch
4949
import kotlinx.coroutines.withContext
5050

51-
//@Sample(
52-
// name = "Selected Photos Access",
53-
// description = "Check and request storage permissions",
54-
// documentation = "https://developer.android.com/about/versions/14/changes/partial-photo-video-access",
55-
//)
56-
//@RequiresPermission(anyOf = [READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_EXTERNAL_STORAGE])
5751
@Composable
5852
fun SelectedPhotosAccessSample() {
5953
val context = LocalContext.current

Diff for: samples/storage/src/main/java/com/example/platform/storage/photopicker/PhotoPicker.kt

-6
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,6 @@ import coil.compose.AsyncImage
5959
* picker. Check out the AndroidManifest.xml of the storage section to see the <service> declaration
6060
* that enables backport support on Android KitKat onwards using Google Play Services
6161
*/
62-
@OptIn(ExperimentalMaterial3Api::class)
63-
//@Sample(
64-
// name = "PhotoPicker",
65-
// description = "Select images/videos in a privacy-friendly way using the photo picker",
66-
// documentation = "https://developer.android.com/training/data-storage/shared/photopicker",
67-
//)
6862
@Composable
6963
fun PhotoPickerSample() {
7064
var selectedMedia by remember { mutableStateOf(emptyList<Uri>()) }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright 2023 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+
* https://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+
17+
package com.example.platform.storage.storageaccessframework
18+
19+
import androidx.activity.compose.rememberLauncherForActivityResult
20+
import androidx.activity.result.contract.ActivityResultContracts
21+
import androidx.compose.foundation.layout.Box
22+
import androidx.compose.foundation.layout.fillMaxSize
23+
import androidx.compose.foundation.layout.padding
24+
import androidx.compose.foundation.layout.size
25+
import androidx.compose.foundation.layout.wrapContentSize
26+
import androidx.compose.foundation.lazy.LazyColumn
27+
import androidx.compose.foundation.lazy.items
28+
import androidx.compose.foundation.rememberScrollState
29+
import androidx.compose.material.icons.Icons
30+
import androidx.compose.material.icons.filled.Check
31+
import androidx.compose.material.icons.outlined.Check
32+
import androidx.compose.material3.DropdownMenu
33+
import androidx.compose.material3.DropdownMenuItem
34+
import androidx.compose.material3.ExtendedFloatingActionButton
35+
import androidx.compose.material3.HorizontalDivider
36+
import androidx.compose.material3.Icon
37+
import androidx.compose.material3.IconButton
38+
import androidx.compose.material3.ListItem
39+
import androidx.compose.material3.Scaffold
40+
import androidx.compose.material3.Switch
41+
import androidx.compose.material3.SwitchDefaults
42+
import androidx.compose.material3.Text
43+
import androidx.compose.runtime.Composable
44+
import androidx.compose.runtime.LaunchedEffect
45+
import androidx.compose.runtime.getValue
46+
import androidx.compose.runtime.mutableStateOf
47+
import androidx.compose.runtime.remember
48+
import androidx.compose.runtime.rememberCoroutineScope
49+
import androidx.compose.runtime.setValue
50+
import androidx.compose.ui.Alignment
51+
import androidx.compose.ui.Modifier
52+
import androidx.compose.ui.platform.LocalContext
53+
import androidx.compose.ui.res.painterResource
54+
import androidx.compose.ui.semantics.contentDescription
55+
import androidx.compose.ui.semantics.semantics
56+
import com.example.platform.storage.R
57+
import com.example.platform.storage.storageaccessframework.shared.AudioFileCard
58+
import com.example.platform.storage.storageaccessframework.shared.BinaryFileCard
59+
import com.example.platform.storage.storageaccessframework.shared.FileRecord
60+
import com.example.platform.storage.storageaccessframework.shared.FileType
61+
import com.example.platform.storage.storageaccessframework.shared.ImageFileCard
62+
import com.example.platform.storage.storageaccessframework.shared.PdfFileCard
63+
import com.example.platform.storage.storageaccessframework.shared.TextFileCard
64+
import com.example.platform.storage.storageaccessframework.shared.VideoFileCard
65+
import kotlinx.coroutines.launch
66+
67+
@Composable
68+
fun GetContentSample() {
69+
val coroutineScope = rememberCoroutineScope()
70+
val context = LocalContext.current
71+
var selectedFilter by remember { mutableStateOf(FileType.Any) }
72+
var selectMultiple by remember { mutableStateOf(false) }
73+
var expanded by remember { mutableStateOf(false) }
74+
var selectedFiles by remember { mutableStateOf(emptyList<FileRecord>()) }
75+
76+
val getSingleDocument =
77+
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
78+
coroutineScope.launch {
79+
selectedFiles = uri?.let { uri ->
80+
FileRecord.fromUri(uri, context)?.let { listOf(it) }
81+
} ?: emptyList()
82+
}
83+
}
84+
85+
val getMultipleDocuments =
86+
rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
87+
coroutineScope.launch {
88+
selectedFiles = uris.mapNotNull { uri ->
89+
FileRecord.fromUri(uri, context)
90+
}
91+
}
92+
}
93+
94+
Scaffold(
95+
modifier = Modifier.fillMaxSize(),
96+
floatingActionButton = {
97+
ExtendedFloatingActionButton(
98+
onClick = {
99+
if (selectMultiple) {
100+
getMultipleDocuments.launch(selectedFilter.mimeType)
101+
} else {
102+
getSingleDocument.launch(selectedFilter.mimeType)
103+
}
104+
},
105+
) {
106+
Text(if (selectMultiple) "Select Files" else "Select File")
107+
}
108+
},
109+
) { paddingValues ->
110+
LazyColumn(Modifier.padding(paddingValues)) {
111+
item {
112+
ListItem(
113+
headlineContent = { Text("File type filter") },
114+
supportingContent = {
115+
Text(selectedFilter.name)
116+
},
117+
trailingContent = {
118+
val scrollState = rememberScrollState()
119+
Box(
120+
modifier = Modifier
121+
.wrapContentSize(Alignment.TopStart),
122+
) {
123+
IconButton(onClick = { expanded = true }) {
124+
Icon(
125+
painter = painterResource(R.drawable.ic_filter_alt_24),
126+
contentDescription = "Localized description",
127+
)
128+
}
129+
DropdownMenu(
130+
expanded = expanded,
131+
onDismissRequest = { expanded = false },
132+
scrollState = scrollState,
133+
) {
134+
FileType.entries.forEach { fileType ->
135+
DropdownMenuItem(
136+
text = { Text(fileType.name) },
137+
onClick = { selectedFilter = fileType },
138+
leadingIcon = {
139+
if (selectedFilter == fileType) {
140+
Icon(
141+
Icons.Outlined.Check,
142+
contentDescription = "Selected",
143+
)
144+
}
145+
},
146+
)
147+
}
148+
}
149+
LaunchedEffect(expanded) {
150+
if (expanded) {
151+
// Scroll to show the bottom menu items.
152+
scrollState.scrollTo(scrollState.maxValue)
153+
}
154+
}
155+
}
156+
},
157+
)
158+
HorizontalDivider()
159+
}
160+
item {
161+
ListItem(
162+
headlineContent = { Text("Select multiple files?") },
163+
trailingContent = {
164+
Switch(
165+
modifier = Modifier.semantics {
166+
contentDescription = "Select multiple files"
167+
},
168+
checked = selectMultiple,
169+
onCheckedChange = { selectMultiple = it },
170+
thumbContent = {
171+
if (selectMultiple) {
172+
Icon(
173+
imageVector = Icons.Filled.Check,
174+
contentDescription = null,
175+
modifier = Modifier.size(SwitchDefaults.IconSize),
176+
)
177+
}
178+
},
179+
)
180+
},
181+
)
182+
HorizontalDivider()
183+
}
184+
items(selectedFiles) { file ->
185+
when (file.fileType) {
186+
FileType.Image -> ImageFileCard(file)
187+
FileType.Video -> VideoFileCard(file)
188+
FileType.Audio -> AudioFileCard(file)
189+
FileType.Text -> TextFileCard(file)
190+
FileType.Pdf -> PdfFileCard(file)
191+
FileType.Any -> BinaryFileCard(file)
192+
}
193+
}
194+
}
195+
}
196+
}

0 commit comments

Comments
 (0)