Skip to content

Commit 802521d

Browse files
Expose StorageHelper and AttachmentFilter to fix StorageHelperWrapper not instantiable (#5954)
* Expose `StorageHelper` and `AttachmentFilter` to fix `StorageHelperWrapper` not instantiable. * Update CHANGELOG.md. * Simplify StorageHelperWrapper instantiation logic. --------- Co-authored-by: André Mion <[email protected]>
1 parent a9b4c12 commit 802521d

File tree

10 files changed

+137
-48
lines changed

10 files changed

+137
-48
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Fix `Message.channelInfo` not populated when parsing `message.new` event. [#5953
5757
### 🐞 Fixed
5858
- Fix unread separator showing before an uncommitted pending message. [#5945](https://github.com/GetStream/stream-chat-android/pull/5945)
5959
- Fix unread separator showing before a message that was sent while offline. [#5945](https://github.com/GetStream/stream-chat-android/pull/5945)
60+
- Expose `StorageHelper` and `AttachmentFilter` as public to fix `StorageHelperWrapper` not instantiable. [#5954](https://github.com/GetStream/stream-chat-android/pull/5954)
6061

6162
### ⬆️ Improved
6263

stream-chat-android-compose/api/stream-chat-android-compose.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4669,6 +4669,7 @@ public final class io/getstream/chat/android/compose/ui/util/SearchResultNameFor
46694669
public final class io/getstream/chat/android/compose/ui/util/StorageHelperWrapper {
46704670
public static final field $stable I
46714671
public fun <init> (Landroid/content/Context;Lio/getstream/chat/android/ui/common/helper/internal/StorageHelper;Lio/getstream/chat/android/ui/common/helper/internal/AttachmentFilter;)V
4672+
public synthetic fun <init> (Landroid/content/Context;Lio/getstream/chat/android/ui/common/helper/internal/StorageHelper;Lio/getstream/chat/android/ui/common/helper/internal/AttachmentFilter;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
46724673
public final fun getAttachmentsForUpload (Ljava/util/List;)Ljava/util/List;
46734674
public final fun getAttachmentsFromUris (Ljava/util/List;)Ljava/util/List;
46744675
public final fun getAttachmentsMetadataFromUris (Ljava/util/List;)Ljava/util/List;

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/factory/AttachmentsPickerFilesTabFactory.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ import io.getstream.chat.android.compose.state.messages.attachments.Files
5151
import io.getstream.chat.android.compose.ui.components.attachments.files.FilesPicker
5252
import io.getstream.chat.android.compose.ui.theme.ChatTheme
5353
import io.getstream.chat.android.compose.ui.util.StorageHelperWrapper
54-
import io.getstream.chat.android.ui.common.helper.internal.AttachmentFilter
55-
import io.getstream.chat.android.ui.common.helper.internal.StorageHelper
5654
import io.getstream.chat.android.ui.common.permissions.FilesAccess
5755
import io.getstream.chat.android.ui.common.permissions.Permissions
5856
import io.getstream.chat.android.ui.common.state.messages.composer.AttachmentMetaData
@@ -109,7 +107,7 @@ public class AttachmentsPickerFilesTabFactory : AttachmentsPickerTabFactory {
109107
val context = LocalContext.current
110108
val lifecycleOwner = LocalLifecycleOwner.current
111109
val storageHelper: StorageHelperWrapper = remember {
112-
StorageHelperWrapper(context, StorageHelper(), AttachmentFilter())
110+
StorageHelperWrapper(context)
113111
}
114112
var showPermanentlyDeniedSnackBar by remember { mutableStateOf(false) }
115113
val permissionLauncher =

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/factory/AttachmentsPickerImagesTabFactory.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ import io.getstream.chat.android.compose.state.messages.attachments.Images
4242
import io.getstream.chat.android.compose.ui.components.attachments.images.ImagesPicker
4343
import io.getstream.chat.android.compose.ui.theme.ChatTheme
4444
import io.getstream.chat.android.compose.ui.util.StorageHelperWrapper
45-
import io.getstream.chat.android.ui.common.helper.internal.AttachmentFilter
46-
import io.getstream.chat.android.ui.common.helper.internal.StorageHelper
4745
import io.getstream.chat.android.ui.common.permissions.Permissions
4846
import io.getstream.chat.android.ui.common.permissions.VisualMediaAccess
4947
import io.getstream.chat.android.ui.common.state.messages.composer.AttachmentMetaData
@@ -101,7 +99,7 @@ public class AttachmentsPickerImagesTabFactory : AttachmentsPickerTabFactory {
10199
val context = LocalContext.current
102100
val lifecycleOwner = LocalLifecycleOwner.current
103101
val storageHelper: StorageHelperWrapper =
104-
remember { StorageHelperWrapper(context, StorageHelper(), AttachmentFilter()) }
102+
remember { StorageHelperWrapper(context) }
105103
val mediaAccess by visualMediaAccessAsState(context, lifecycleOwner) { value ->
106104
if (value != VisualMediaAccess.DENIED) {
107105
val media = storageHelper.getMedia()

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/factory/AttachmentsPickerSystemTabFactory.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme
7777
import io.getstream.chat.android.compose.ui.util.StorageHelperWrapper
7878
import io.getstream.chat.android.compose.ui.util.clickable
7979
import io.getstream.chat.android.ui.common.helper.internal.AttachmentFilter
80-
import io.getstream.chat.android.ui.common.helper.internal.StorageHelper
8180
import io.getstream.chat.android.ui.common.permissions.SystemAttachmentsPickerConfig
8281
import io.getstream.chat.android.ui.common.permissions.toContractVisualMediaType
8382
import io.getstream.chat.android.ui.common.state.messages.composer.AttachmentMetaData
@@ -210,7 +209,7 @@ public class AttachmentsPickerSystemTabFactory(
210209
) {
211210
val context = LocalContext.current
212211
val storageHelper: StorageHelperWrapper = remember {
213-
StorageHelperWrapper(context, StorageHelper(), AttachmentFilter())
212+
StorageHelperWrapper(context)
214213
}
215214

216215
val filePickerLauncher = rememberFilePickerLauncher { uri ->

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/StorageHelperWrapper.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ import io.getstream.chat.android.ui.common.state.messages.composer.AttachmentMet
3535
*/
3636
public class StorageHelperWrapper(
3737
private val context: Context,
38-
private val storageHelper: StorageHelper,
39-
private val attachmentFilter: AttachmentFilter,
38+
private val storageHelper: StorageHelper = StorageHelper(),
39+
private val attachmentFilter: AttachmentFilter = AttachmentFilter(),
4040
) {
4141

4242
/**

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ import io.getstream.chat.android.ui.common.feature.messages.list.MessageListCont
3636
import io.getstream.chat.android.ui.common.feature.messages.list.MessagePositionHandler
3737
import io.getstream.chat.android.ui.common.helper.ClipboardHandler
3838
import io.getstream.chat.android.ui.common.helper.ClipboardHandlerImpl
39-
import io.getstream.chat.android.ui.common.helper.internal.AttachmentFilter
40-
import io.getstream.chat.android.ui.common.helper.internal.StorageHelper
4139
import io.getstream.chat.android.ui.common.state.messages.list.DeletedMessageVisibility
4240
import io.getstream.chat.android.ui.common.state.messages.list.MessageFooterVisibility
4341
import io.getstream.chat.android.ui.common.utils.AttachmentConstants
@@ -163,11 +161,7 @@ public class MessagesViewModelFactory(
163161
)
164162
},
165163
AttachmentsPickerViewModel::class.java to {
166-
AttachmentsPickerViewModel(
167-
StorageHelperWrapper(context, StorageHelper(), AttachmentFilter()),
168-
channelStateFlow,
169-
170-
)
164+
AttachmentsPickerViewModel(StorageHelperWrapper(context), channelStateFlow)
171165
},
172166
)
173167

stream-chat-android-ui-common/api/stream-chat-android-ui-common.api

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,27 @@ public abstract interface class io/getstream/chat/android/ui/common/helper/Video
11051105
public abstract fun getVideoRequestHeaders (Ljava/lang/String;)Ljava/util/Map;
11061106
}
11071107

1108+
public final class io/getstream/chat/android/ui/common/helper/internal/AttachmentFilter {
1109+
public static final field $stable I
1110+
public fun <init> ()V
1111+
public fun <init> (Lio/getstream/chat/android/client/ChatClient;)V
1112+
public synthetic fun <init> (Lio/getstream/chat/android/client/ChatClient;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
1113+
public final fun filterAttachments (Ljava/util/List;)Ljava/util/List;
1114+
public final fun getSupportedMimeTypes ()Ljava/util/List;
1115+
}
1116+
1117+
public final class io/getstream/chat/android/ui/common/helper/internal/StorageHelper {
1118+
public static final field $stable I
1119+
public static final field Companion Lio/getstream/chat/android/ui/common/helper/internal/StorageHelper$Companion;
1120+
public static final field FILE_NAME_PREFIX Ljava/lang/String;
1121+
public static final field TIME_FORMAT Ljava/lang/String;
1122+
public fun <init> ()V
1123+
public final fun getAttachmentsFromUriList (Landroid/content/Context;Ljava/util/List;)Ljava/util/List;
1124+
public final fun getCachedFileFromUri (Landroid/content/Context;Lio/getstream/chat/android/ui/common/state/messages/composer/AttachmentMetaData;)Ljava/io/File;
1125+
public final fun getFileAttachments (Landroid/content/Context;)Ljava/util/List;
1126+
public final fun getMediaAttachments (Landroid/content/Context;)Ljava/util/List;
1127+
}
1128+
11081129
public final class io/getstream/chat/android/ui/common/helper/internal/StorageHelper$Companion {
11091130
}
11101131

stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/helper/internal/AttachmentFilter.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package io.getstream.chat.android.ui.common.helper.internal
1818

1919
import androidx.core.content.MimeTypeFilter
2020
import io.getstream.chat.android.client.ChatClient
21-
import io.getstream.chat.android.core.internal.InternalStreamChatApi
2221
import io.getstream.chat.android.models.AttachmentType
2322
import io.getstream.chat.android.ui.common.state.messages.composer.AttachmentMetaData
2423
import io.getstream.log.taggedLogger
@@ -31,7 +30,6 @@ import io.getstream.log.taggedLogger
3130
*
3231
* @param chatClient An instance of the low level chat client to fetch upload config.
3332
*/
34-
@InternalStreamChatApi
3533
public class AttachmentFilter(
3634
private val chatClient: ChatClient = ChatClient.instance(),
3735
) {

stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/helper/internal/StorageHelper.kt

Lines changed: 108 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,47 @@ import android.database.Cursor
2222
import android.net.Uri
2323
import android.provider.MediaStore
2424
import android.webkit.MimeTypeMap
25-
import io.getstream.chat.android.core.internal.InternalStreamChatApi
2625
import io.getstream.chat.android.models.AttachmentType
2726
import io.getstream.chat.android.ui.common.state.messages.composer.AttachmentMetaData
2827
import java.io.File
2928
import java.text.SimpleDateFormat
3029
import java.util.Date
3130
import java.util.Locale
3231

33-
@InternalStreamChatApi
32+
/**
33+
* Helper class for managing file and media attachments from device storage.
34+
*
35+
* This class provides utilities to:
36+
* - Query files and media from the device's MediaStore
37+
* - Cache remote or content URI files to local storage
38+
* - Parse attachment metadata from content URIs
39+
*
40+
* The class uses Android's MediaStore API to retrieve file information and should be called
41+
* from a background thread when querying large datasets to avoid blocking the main thread.
42+
*
43+
* @see AttachmentMetaData
44+
*/
3445
@Suppress("TooManyFunctions")
3546
public class StorageHelper {
3647
private val dateFormat = SimpleDateFormat(TIME_FORMAT, Locale.US)
3748

49+
/**
50+
* Retrieves or creates a cached copy of a file from the given attachment metadata.
51+
*
52+
* This method handles two scenarios:
53+
* 1. If [AttachmentMetaData.file] is already set, it returns the file directly
54+
* 2. If only [AttachmentMetaData.uri] is available, it copies the content to a cache file
55+
*
56+
* The cached file is stored in a unique timestamped folder within the app's cache directory
57+
* to prevent naming conflicts. The file name is derived from the attachment's title with
58+
* proper extension handling.
59+
*
60+
* @param context The Android context used to access the content resolver and cache directory.
61+
* @param attachmentMetaData The attachment metadata containing either a file or URI reference.
62+
* @return A [File] object pointing to the cached file, or `null` if both file and URI are null.
63+
*
64+
* @throws java.io.IOException If there's an error reading from the URI or writing to cache.
65+
*/
3866
public fun getCachedFileFromUri(
3967
context: Context,
4068
attachmentMetaData: AttachmentMetaData,
@@ -55,6 +83,22 @@ public class StorageHelper {
5583
return cachedFile
5684
}
5785

86+
/**
87+
* Retrieves all file attachments from the device's external storage.
88+
*
89+
* This method queries the MediaStore for all files that have a valid MIME type, excluding
90+
* folders and files with unknown types. The results are sorted by date added in descending
91+
* order (most recent first).
92+
*
93+
* Note: This method performs a potentially expensive query operation and should be called
94+
* from a background thread to avoid blocking the UI.
95+
*
96+
* @param context The Android context used to access the content resolver.
97+
* @return A list of [AttachmentMetaData] objects representing all files on the device,
98+
* or an empty list if the query fails or returns no results.
99+
*
100+
* @see getMediaAttachments For retrieving only images and videos.
101+
*/
58102
public fun getFileAttachments(context: Context): List<AttachmentMetaData> {
59103
// Excluding files with empty mime type just to be sure that we won't include folder and unknown files
60104
return getFilteredAttachments(
@@ -64,6 +108,25 @@ public class StorageHelper {
64108
)
65109
}
66110

111+
/**
112+
* Retrieves all media attachments (images and videos) from the device's external storage.
113+
*
114+
* This method queries the MediaStore specifically for files with media type IMAGE or VIDEO,
115+
* filtering out all other file types. The results are sorted by date added in descending
116+
* order (most recent first).
117+
*
118+
* The returned metadata includes video duration for video files, which is useful for
119+
* displaying in the UI or for validation purposes.
120+
*
121+
* Note: This method performs a potentially expensive query operation and should be called
122+
* from a background thread to avoid blocking the UI.
123+
*
124+
* @param context The Android context used to access the content resolver.
125+
* @return A list of [AttachmentMetaData] objects representing all images and videos on the device,
126+
* or an empty list if the query fails or returns no results.
127+
*
128+
* @see getFileAttachments For retrieving all file types.
129+
*/
67130
public fun getMediaAttachments(context: Context): List<AttachmentMetaData> {
68131
val selection = (
69132
MediaStore.Files.FileColumns.MEDIA_TYPE + "=" +
@@ -75,35 +138,27 @@ public class StorageHelper {
75138
return getFilteredAttachments(context, selection)
76139
}
77140

78-
private fun getFilteredAttachments(context: Context, selection: String?): List<AttachmentMetaData> {
79-
val columns = arrayOf(
80-
MediaStore.Files.FileColumns._ID,
81-
MediaStore.Files.FileColumns.DISPLAY_NAME,
82-
MediaStore.Files.FileColumns.MIME_TYPE,
83-
MediaStore.Files.FileColumns.SIZE,
84-
MediaStore.Files.FileColumns.DURATION,
85-
)
86-
context.contentResolver.query(
87-
MediaStore.Files.getContentUri("external"),
88-
columns,
89-
selection,
90-
null,
91-
"${MediaStore.Files.FileColumns.DATE_ADDED} DESC",
92-
)?.use { cursor ->
93-
return mutableListOf<AttachmentMetaData>().apply {
94-
while (cursor.moveToNext()) {
95-
add(getAttachmentFromCursor(cursor))
96-
}
97-
}
98-
}
99-
return emptyList()
100-
}
101-
102141
/**
103-
* Queries the given list of content URI and returns the parsed metadata.
142+
* Queries a list of content URIs and returns parsed attachment metadata for each.
104143
*
105-
* @param uriList The list of URIs, using the content:// scheme.
106-
* @return A list of objects with parsed metadata for the list of URIs.
144+
* This method is useful when you have specific URIs (e.g., from an intent or file picker)
145+
* and need to extract their metadata for attachment processing. Each URI is queried
146+
* individually using the content resolver to retrieve:
147+
* - Display name
148+
* - MIME type (with fallback to content resolver type)
149+
* - File size
150+
*
151+
* URIs that cannot be queried or return no data are filtered out from the result.
152+
* The attachment type (image, video, or file) is automatically determined based on
153+
* the MIME type.
154+
*
155+
* @param context The Android context used to access the content resolver.
156+
* @param uriList The list of content URIs (using the `content://` scheme) to query.
157+
* @return A list of [AttachmentMetaData] objects with parsed metadata. URIs that fail
158+
* to resolve are omitted from the result.
159+
*
160+
* @see getFileAttachments For querying all files from device storage.
161+
* @see getMediaAttachments For querying all media from device storage.
107162
*/
108163
public fun getAttachmentsFromUriList(context: Context, uriList: List<Uri>): List<AttachmentMetaData> {
109164
return uriList.mapNotNull { uri ->
@@ -152,6 +207,30 @@ public class StorageHelper {
152207
}
153208
}
154209

210+
private fun getFilteredAttachments(context: Context, selection: String?): List<AttachmentMetaData> {
211+
val columns = arrayOf(
212+
MediaStore.Files.FileColumns._ID,
213+
MediaStore.Files.FileColumns.DISPLAY_NAME,
214+
MediaStore.Files.FileColumns.MIME_TYPE,
215+
MediaStore.Files.FileColumns.SIZE,
216+
MediaStore.Files.FileColumns.DURATION,
217+
)
218+
context.contentResolver.query(
219+
MediaStore.Files.getContentUri("external"),
220+
columns,
221+
selection,
222+
null,
223+
"${MediaStore.Files.FileColumns.DATE_ADDED} DESC",
224+
)?.use { cursor ->
225+
return mutableListOf<AttachmentMetaData>().apply {
226+
while (cursor.moveToNext()) {
227+
add(getAttachmentFromCursor(cursor))
228+
}
229+
}
230+
}
231+
return emptyList()
232+
}
233+
155234
private fun getAttachmentFromCursor(cursor: Cursor): AttachmentMetaData {
156235
val displayNameIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.DISPLAY_NAME)
157236
val fileSizeIndex = cursor.getColumnIndex(MediaStore.Files.FileColumns.SIZE)

0 commit comments

Comments
 (0)