Skip to content

Commit 3a6fce3

Browse files
Implement basic ACTION_SEND_MULTIPLE UI
1 parent fbc6cdf commit 3a6fce3

File tree

9 files changed

+471
-141
lines changed

9 files changed

+471
-141
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@
4646
<data android:mimeType="*/*" />
4747
</intent-filter>
4848

49+
<intent-filter>
50+
<action android:name="android.intent.action.SEND_MULTIPLE" />
51+
<category android:name="android.intent.category.DEFAULT" />
52+
<data android:mimeType="*/*" />
53+
</intent-filter>
54+
4955
</activity>
5056

5157
<activity-alias
@@ -64,4 +70,4 @@
6470

6571
</application>
6672

67-
</manifest>
73+
</manifest>

app/src/main/kotlin/com/mateusrodcosta/apps/share2storage/DetailsActivity.kt

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import androidx.lifecycle.lifecycleScope
3636
import com.mateusrodcosta.apps.share2storage.model.UriData
3737
import com.mateusrodcosta.apps.share2storage.screens.DetailsScreen
3838
import com.mateusrodcosta.apps.share2storage.screens.DetailsScreenSkipped
39+
import com.mateusrodcosta.apps.share2storage.screens.MultipleDetailsScreen
3940
import com.mateusrodcosta.apps.share2storage.utils.CreateDocumentWithInitialUri
4041
import com.mateusrodcosta.apps.share2storage.utils.SharedPreferenceKeys
4142
import com.mateusrodcosta.apps.share2storage.utils.SharedPreferenceUtils
@@ -53,6 +54,8 @@ class DetailsActivity : ComponentActivity() {
5354
private var uriData: UriData? = null
5455
private var content: CharSequence? = null
5556

57+
private var uriDataList: List<UriData>? = null
58+
5659
private var defaultSaveLocation: Uri? = null
5760
private var shouldSkipFilePicker: Boolean = false
5861
private var skipFileDetails: Boolean = false
@@ -91,17 +94,22 @@ class DetailsActivity : ComponentActivity() {
9194
setContent {
9295
val windowSizeClass = calculateWindowSizeClass(this)
9396

94-
if (skipFileDetails) {
95-
LaunchedEffect(Unit) {
96-
launchFilePicker()
97-
}
98-
DetailsScreenSkipped()
97+
if (uriDataList != null) {
98+
MultipleDetailsScreen(uriDataList, windowSizeClass)
9999
} else {
100-
DetailsScreen(
101-
uriData,
102-
windowSizeClass,
103-
launchFilePicker,
104-
)
100+
101+
if (skipFileDetails) {
102+
LaunchedEffect(Unit) {
103+
launchFilePicker()
104+
}
105+
DetailsScreenSkipped()
106+
} else {
107+
DetailsScreen(
108+
uriData,
109+
windowSizeClass,
110+
launchFilePicker,
111+
)
112+
}
105113
}
106114
}
107115
}
@@ -226,6 +234,11 @@ class DetailsActivity : ComponentActivity() {
226234
}
227235
}
228236
}
237+
238+
uriList?.let {
239+
val uriDataList = it.mapNotNull { item -> getUriData(contentResolver, item) }
240+
this.uriDataList = uriDataList
241+
}
229242
}
230243

231244
private suspend fun handleFileSave(uri: Uri, fileUri: Uri) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (C) 2025 Mateus Rodrigues Costa
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License, or (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.mateusrodcosta.apps.share2storage.model
19+
20+
object SampleData {
21+
val bastionSample = UriData(
22+
uri = null,
23+
displayName = "21. Setting Sail, Coming Home (End Theme).flac",
24+
mimeType = "audio/flac",
25+
size = 35280673,
26+
)
27+
28+
val katamariSample = UriData(
29+
uri = null,
30+
displayName = "03. Lonely Rolling Star (Missing You).flac",
31+
mimeType = "audio/flac",
32+
size = 41123343,
33+
)
34+
35+
val nullSample = null
36+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (C) 2025 Mateus Rodrigues Costa
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License, or (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.mateusrodcosta.apps.share2storage.model
19+
20+
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
21+
22+
class SampleUriDataListProvider : PreviewParameterProvider<List<UriData>?> {
23+
override val values = sequenceOf(
24+
listOfNotNull(
25+
SampleData.bastionSample, SampleData.katamariSample, SampleData.nullSample
26+
), null
27+
)
28+
}

app/src/main/kotlin/com/mateusrodcosta/apps/share2storage/model/SampleUriDataProvider.kt

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
2121

2222
class SampleUriDataProvider : PreviewParameterProvider<UriData?> {
2323
override val values = sequenceOf(
24-
UriData(
25-
uri = null,
26-
displayName = "21. Setting Sail, Coming Home (End Theme).flac",
27-
mimeType = "audio/flac",
28-
size = 35280673,
29-
),
30-
UriData(
31-
uri = null,
32-
displayName = "03. Lonely Rolling Star (Missing You).flac",
33-
mimeType = "audio/flac",
34-
size = 41123343,
35-
),
36-
null,
24+
SampleData.bastionSample, SampleData.katamariSample, SampleData.nullSample
3725
)
3826
}

app/src/main/kotlin/com/mateusrodcosta/apps/share2storage/screens/DetailsScreen.kt

Lines changed: 8 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2022 - 2024 Mateus Rodrigues Costa
2+
* Copyright (C) 2022 - 2025 Mateus Rodrigues Costa
33
*
44
* This program is free software: you can redistribute it and/or modify
55
* it under the terms of the GNU Affero General Public License as
@@ -17,8 +17,6 @@
1717

1818
package com.mateusrodcosta.apps.share2storage.screens
1919

20-
import android.text.format.Formatter
21-
import androidx.compose.foundation.clickable
2220
import androidx.compose.foundation.layout.Arrangement
2321
import androidx.compose.foundation.layout.Box
2422
import androidx.compose.foundation.layout.Column
@@ -28,22 +26,15 @@ import androidx.compose.foundation.layout.WindowInsetsSides
2826
import androidx.compose.foundation.layout.fillMaxSize
2927
import androidx.compose.foundation.layout.only
3028
import androidx.compose.foundation.layout.padding
31-
import androidx.compose.foundation.layout.size
3229
import androidx.compose.foundation.layout.systemBars
3330
import androidx.compose.foundation.layout.windowInsetsPadding
3431
import androidx.compose.foundation.rememberScrollState
3532
import androidx.compose.foundation.verticalScroll
3633
import androidx.compose.material.icons.Icons
37-
import androidx.compose.material.icons.outlined.AudioFile
38-
import androidx.compose.material.icons.outlined.Description
39-
import androidx.compose.material.icons.outlined.Image
40-
import androidx.compose.material.icons.outlined.VideoFile
4134
import androidx.compose.material.icons.rounded.Download
42-
import androidx.compose.material3.CircularProgressIndicator
4335
import androidx.compose.material3.ExperimentalMaterial3Api
4436
import androidx.compose.material3.FloatingActionButton
4537
import androidx.compose.material3.Icon
46-
import androidx.compose.material3.ListItem
4738
import androidx.compose.material3.MaterialTheme
4839
import androidx.compose.material3.Scaffold
4940
import androidx.compose.material3.Text
@@ -52,22 +43,16 @@ import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
5243
import androidx.compose.material3.windowsizeclass.WindowSizeClass
5344
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
5445
import androidx.compose.runtime.Composable
55-
import androidx.compose.runtime.collectAsState
56-
import androidx.compose.runtime.getValue
5746
import androidx.compose.ui.Alignment
5847
import androidx.compose.ui.Modifier
59-
import androidx.compose.ui.layout.ContentScale
60-
import androidx.compose.ui.platform.LocalContext
6148
import androidx.compose.ui.res.stringResource
6249
import androidx.compose.ui.tooling.preview.Preview
6350
import androidx.compose.ui.tooling.preview.PreviewParameter
64-
import androidx.compose.ui.unit.dp
65-
import coil3.compose.AsyncImagePainter
66-
import coil3.compose.SubcomposeAsyncImage
67-
import coil3.compose.SubcomposeAsyncImageContent
6851
import com.mateusrodcosta.apps.share2storage.R
6952
import com.mateusrodcosta.apps.share2storage.model.SampleUriDataProvider
7053
import com.mateusrodcosta.apps.share2storage.model.UriData
54+
import com.mateusrodcosta.apps.share2storage.screens.shared.FileInfo
55+
import com.mateusrodcosta.apps.share2storage.screens.shared.FilePreview
7156
import com.mateusrodcosta.apps.share2storage.screens.shared.shouldShowLandscape
7257
import com.mateusrodcosta.apps.share2storage.ui.theme.AppTheme
7358

@@ -164,16 +149,8 @@ fun FileDetailsPortrait(uriData: UriData) {
164149
horizontalAlignment = Alignment.CenterHorizontally,
165150
verticalArrangement = Arrangement.SpaceEvenly
166151
) {
167-
Box(
168-
modifier = Modifier
169-
.fillMaxSize()
170-
.weight(1.0f)
171-
) {
172-
FilePreview(uriData)
173-
}
174-
Box {
175-
FileInfo(uriData)
176-
}
152+
FilePreview(uriData, modifier = Modifier.weight(1.0f))
153+
FileInfo(uriData, modifier = Modifier.verticalScroll(rememberScrollState()))
177154
}
178155
}
179156

@@ -184,98 +161,13 @@ fun FileDetailsLandscape(uriData: UriData) {
184161
horizontalArrangement = Arrangement.SpaceEvenly,
185162
verticalAlignment = Alignment.CenterVertically,
186163
) {
187-
Box(modifier = Modifier.weight(1.0f)) {
188-
FilePreview(uriData)
189-
}
190-
Box(modifier = Modifier.weight(1.0f)) {
191-
FileInfo(uriData)
192-
}
193-
}
194-
}
195-
196-
@Composable
197-
fun FileInfo(uriData: UriData) {
198-
Column(
199-
modifier = Modifier.verticalScroll(rememberScrollState()),
200-
verticalArrangement = Arrangement.Center
201-
) {
202-
FileInfoLine(
203-
label = stringResource(R.string.file_name),
204-
content = uriData.displayName
205-
)
206-
FileInfoLine(
207-
label = stringResource(R.string.file_type),
208-
content = uriData.mimeType ?: "*/*"
209-
)
210-
FileInfoLine(
211-
label = stringResource(R.string.file_size),
212-
content = Formatter.formatFileSize(LocalContext.current, uriData.size)
164+
FilePreview(
165+
uriData, modifier = Modifier.weight(1.0f)
213166
)
167+
FileInfo(uriData, modifier = Modifier.weight(1.0f))
214168
}
215169
}
216170

217-
@Composable
218-
fun FileInfoLine(label: String, content: String) {
219-
ListItem(modifier = Modifier.clickable { }, headlineContent = {
220-
Text(label)
221-
}, supportingContent = {
222-
Text(content, softWrap = true)
223-
})
224-
}
225-
226-
@Composable
227-
fun FilePreview(uriData: UriData) {
228-
val mimeType = uriData.mimeType
229-
val primaryType = mimeType?.substringBefore('/')
230-
231-
val fallbackFileIcon = when (primaryType) {
232-
"image" -> Icons.Outlined.Image
233-
"audio" -> Icons.Outlined.AudioFile
234-
"video" -> Icons.Outlined.VideoFile
235-
else -> Icons.Outlined.Description
236-
}
237-
val fallbackFileLabel = when (primaryType) {
238-
"image" -> "Image"
239-
"audio" -> "Audio"
240-
"video" -> "Video"
241-
else -> "File"
242-
}
243-
244-
Box(
245-
modifier = Modifier
246-
.fillMaxSize()
247-
.padding(16.dp)
248-
) {
249-
SubcomposeAsyncImage(
250-
modifier = Modifier.align(Alignment.Center),
251-
model = uriData.uri,
252-
contentDescription = stringResource(R.string.app_name),
253-
contentScale = ContentScale.Fit,
254-
) {
255-
val state by painter.state.collectAsState()
256-
257-
when (state) {
258-
is AsyncImagePainter.State.Loading -> {
259-
CircularProgressIndicator()
260-
}
261-
// Display fallback icon if can't create thumbnail
262-
is AsyncImagePainter.State.Error -> {
263-
Icon(
264-
modifier = Modifier
265-
.size(128.dp)
266-
.align(Alignment.Center),
267-
imageVector = fallbackFileIcon,
268-
contentDescription = fallbackFileLabel,
269-
tint = MaterialTheme.colorScheme.tertiary
270-
)
271-
}
272-
else -> {
273-
SubcomposeAsyncImageContent()
274-
}
275-
}
276-
}
277-
}
278-
}
279171

280172
@Preview(apiLevel = 34, showSystemUi = true, showBackground = true)
281173
@Composable

0 commit comments

Comments
 (0)