providing image gallery via DI graph

This commit is contained in:
Adam Brown 2022-09-29 16:24:17 +01:00
parent bd885823bd
commit 415ea4b150
7 changed files with 104 additions and 74 deletions

View File

@ -15,6 +15,7 @@ import app.dapk.st.graph.AppModule
import app.dapk.st.home.HomeModule import app.dapk.st.home.HomeModule
import app.dapk.st.login.LoginModule import app.dapk.st.login.LoginModule
import app.dapk.st.messenger.MessengerModule import app.dapk.st.messenger.MessengerModule
import app.dapk.st.messenger.gallery.ImageGalleryModule
import app.dapk.st.notifications.NotificationsModule import app.dapk.st.notifications.NotificationsModule
import app.dapk.st.profile.ProfileModule import app.dapk.st.profile.ProfileModule
import app.dapk.st.push.PushModule import app.dapk.st.push.PushModule
@ -81,6 +82,7 @@ class SmallTalkApplication : Application(), ModuleProvider {
TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule
CoreAndroidModule::class -> appModule.coreAndroidModule CoreAndroidModule::class -> appModule.coreAndroidModule
ShareEntryModule::class -> featureModules.shareEntryModule ShareEntryModule::class -> featureModules.shareEntryModule
ImageGalleryModule::class -> featureModules.imageGalleryModule
else -> throw IllegalArgumentException("Unknown: $klass") else -> throw IllegalArgumentException("Unknown: $klass")
} as T } as T
} }

View File

@ -43,6 +43,7 @@ import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent
import app.dapk.st.matrix.sync.internal.room.MessageDecrypter import app.dapk.st.matrix.sync.internal.room.MessageDecrypter
import app.dapk.st.messenger.MessengerActivity import app.dapk.st.messenger.MessengerActivity
import app.dapk.st.messenger.MessengerModule import app.dapk.st.messenger.MessengerModule
import app.dapk.st.messenger.gallery.ImageGalleryModule
import app.dapk.st.navigator.IntentFactory import app.dapk.st.navigator.IntentFactory
import app.dapk.st.navigator.MessageAttachment import app.dapk.st.navigator.MessageAttachment
import app.dapk.st.notifications.MatrixPushHandler import app.dapk.st.notifications.MatrixPushHandler
@ -217,6 +218,10 @@ internal class FeatureModules internal constructor(
ShareEntryModule(matrixModules.sync, matrixModules.room) ShareEntryModule(matrixModules.sync, matrixModules.room)
} }
val imageGalleryModule by unsafeLazy {
ImageGalleryModule(context.contentResolver, coroutineDispatchers)
}
val pushModule by unsafeLazy { val pushModule by unsafeLazy {
domainModules.pushModule domainModules.pushModule
} }

View File

@ -3,21 +3,17 @@ package app.dapk.st.messenger.gallery
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentUris import android.content.ContentUris
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.provider.MediaStore.Images import android.provider.MediaStore.Images
import kotlinx.coroutines.Dispatchers import app.dapk.st.core.CoroutineDispatchers
import kotlinx.coroutines.withContext import app.dapk.st.core.withIoContext
// https://github.com/signalapp/Signal-Android/blob/e22ddb8f96f8801f0abe622b5261abc6cb396d94/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java
class FetchMediaFoldersUseCase( class FetchMediaFoldersUseCase(
private val contentResolver: ContentResolver, private val contentResolver: ContentResolver,
private val dispatchers: CoroutineDispatchers,
) { ) {
suspend fun fetchFolders(): List<Folder> { suspend fun fetchFolders(): List<Folder> {
return withContext(Dispatchers.IO) { return dispatchers.withIoContext {
val projection = arrayOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED) val projection = arrayOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED)
val selection = "${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?" val selection = "${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?"
val sortBy = "${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC" val sortBy = "${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC"
@ -39,7 +35,6 @@ class FetchMediaFoldersUseCase(
} }
} }
} }
data class Folder( data class Folder(
@ -56,59 +51,3 @@ data class Folder(
} }
} }
class FetchMediaUseCase(private val contentResolver: ContentResolver) {
private val projection = arrayOf(
Images.Media._ID,
Images.Media.MIME_TYPE,
Images.Media.DATE_MODIFIED,
Images.Media.ORIENTATION,
Images.Media.WIDTH,
Images.Media.HEIGHT,
Images.Media.SIZE
)
suspend fun getMediaInBucket(bucketId: String): List<Media> {
return withContext(Dispatchers.IO) {
val media = mutableListOf<Media>()
val selection = Images.Media.BUCKET_ID + " = ? AND " + isNotPending() + " AND " + Images.Media.MIME_TYPE + " NOT LIKE ?"
val selectionArgs = arrayOf(bucketId, "%image/svg%")
val sortBy = Images.Media.DATE_MODIFIED + " DESC"
val contentUri = Images.Media.EXTERNAL_CONTENT_URI
contentResolver.query(contentUri, projection, selection, selectionArgs, sortBy).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]))
val uri = ContentUris.withAppendedId(contentUri, rowId)
val mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE))
val date = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_MODIFIED))
val orientation = cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION))
val width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)))
val height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE))
media.add(Media(rowId, uri, mimetype, width, height, size, date))
}
}
media
}
}
private fun getWidthColumn(orientation: Int) = if (orientation == 0 || orientation == 180) Images.Media.WIDTH else Images.Media.HEIGHT
private fun getHeightColumn(orientation: Int) = if (orientation == 0 || orientation == 180) Images.Media.HEIGHT else Images.Media.WIDTH
}
data class Media(
val id: Long,
val uri: Uri,
val mimeType: String,
val width: Int,
val height: Int,
val size: Long,
val dateModifiedEpochMillis: Long,
)
private fun isNotPending() = if (Build.VERSION.SDK_INT <= 28) Images.Media.DATA + " NOT NULL" else MediaStore.MediaColumns.IS_PENDING + " != 1"

View File

@ -0,0 +1,64 @@
package app.dapk.st.messenger.gallery
import android.content.ContentResolver
import android.content.ContentUris
import android.net.Uri
import android.provider.MediaStore
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.withIoContext
class FetchMediaUseCase(private val contentResolver: ContentResolver, private val dispatchers: CoroutineDispatchers) {
private val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.MIME_TYPE,
MediaStore.Images.Media.DATE_MODIFIED,
MediaStore.Images.Media.ORIENTATION,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.HEIGHT,
MediaStore.Images.Media.SIZE
)
private val selection = MediaStore.Images.Media.BUCKET_ID + " = ? AND " + isNotPending() + " AND " + MediaStore.Images.Media.MIME_TYPE + " NOT LIKE ?"
suspend fun getMediaInBucket(bucketId: String): List<Media> {
return dispatchers.withIoContext {
val media = mutableListOf<Media>()
val selectionArgs = arrayOf(bucketId, "%image/svg%")
val sortBy = MediaStore.Images.Media.DATE_MODIFIED + " DESC"
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
contentResolver.query(contentUri, projection, selection, selectionArgs, sortBy).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]))
val uri = ContentUris.withAppendedId(contentUri, rowId)
val mimetype = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE))
val date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED))
val orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.ORIENTATION))
val width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)))
val height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE))
media.add(Media(rowId, uri, mimetype, width, height, size, date))
}
}
media
}
}
private fun getWidthColumn(orientation: Int) = if (orientation == 0 || orientation == 180) MediaStore.Images.Media.WIDTH else MediaStore.Images.Media.HEIGHT
private fun getHeightColumn(orientation: Int) =
if (orientation == 0 || orientation == 180) MediaStore.Images.Media.HEIGHT else MediaStore.Images.Media.WIDTH
}
data class Media(
val id: Long,
val uri: Uri,
val mimeType: String,
val width: Int,
val height: Int,
val size: Long,
val dateModifiedEpochMillis: Long,
)

View File

@ -12,21 +12,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import app.dapk.st.core.DapkActivity import app.dapk.st.core.*
import app.dapk.st.core.Lce import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.core.PermissionResult import app.dapk.st.messenger.MessengerModule
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ImageGalleryActivity : DapkActivity() { class ImageGalleryActivity : DapkActivity() {
private val module by unsafeLazy { module<ImageGalleryModule>() }
private val viewModel by viewModel { module.imageGalleryViewModel() }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val viewModel = ImageGalleryViewModel(
FetchMediaFoldersUseCase(contentResolver),
FetchMediaUseCase(contentResolver),
)
val permissionState = mutableStateOf<Lce<PermissionResult>>(Lce.Loading()) val permissionState = mutableStateOf<Lce<PermissionResult>>(Lce.Loading())
lifecycleScope.launch { lifecycleScope.launch {

View File

@ -0,0 +1,17 @@
package app.dapk.st.messenger.gallery
import android.content.ContentResolver
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.ProvidableModule
class ImageGalleryModule(
private val contentResolver: ContentResolver,
private val dispatchers: CoroutineDispatchers,
) : ProvidableModule {
fun imageGalleryViewModel() = ImageGalleryViewModel(
FetchMediaFoldersUseCase(contentResolver, dispatchers),
FetchMediaUseCase(contentResolver, dispatchers),
)
}

View File

@ -0,0 +1,6 @@
package app.dapk.st.messenger.gallery
import android.os.Build
import android.provider.MediaStore
fun isNotPending() = if (Build.VERSION.SDK_INT <= 28) MediaStore.Images.Media.DATA + " NOT NULL" else MediaStore.MediaColumns.IS_PENDING + " != 1"