Skip to content

Commit

Permalink
[Setting] 갤러리 ui & 붙이기 (#51)
Browse files Browse the repository at this point in the history
* [Chore]: 갤러리 관련 업무

* [Feat]: 갤러리 ui 구현 & 기초 세팅

* [Chore]: 라이브러리,권한 추가

* [Feat]: 갤러리 페이징 기능 구현

* [Fix]: 오류 수정

* [Style]: 패키지명 수정, 코드 포맷 변경

* [Chore]: 코드 포맷 변경

* [Fix]: 오류 수정
  • Loading branch information
jinuemong authored Feb 6, 2024
1 parent 4c3a6e5 commit 778c6fe
Show file tree
Hide file tree
Showing 20 changed files with 707 additions and 7 deletions.
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission-sdk-23 android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<application
android:name=".BookkeepingApplication"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package ac.dnd.bookkeeping.android.data.di

import ac.dnd.bookkeeping.android.data.remote.local.gallery.GalleryImageRepositoryImpl
import ac.dnd.bookkeeping.android.data.repository.authentication.MockAuthenticationRepository
import ac.dnd.bookkeeping.android.data.repository.file.MockFileRepository
import ac.dnd.bookkeeping.android.data.repository.authentication.sociallogin.KakaoLoginRepositoryImpl
import ac.dnd.bookkeeping.android.data.repository.feature.group.MockGroupRepository
import ac.dnd.bookkeeping.android.data.repository.feature.heart.MockHeartRepository
import ac.dnd.bookkeeping.android.data.repository.member.MockMemberRepository
import ac.dnd.bookkeeping.android.data.repository.feature.relation.MockRelationRepository
import ac.dnd.bookkeeping.android.data.repository.feature.schedule.MockScheduleRepository
import ac.dnd.bookkeeping.android.data.repository.authentication.sociallogin.KakaoLoginRepositoryImpl
import ac.dnd.bookkeeping.android.data.repository.feature.statistics.MockStatisticsRepository
import ac.dnd.bookkeeping.android.data.repository.file.MockFileRepository
import ac.dnd.bookkeeping.android.data.repository.gallery.GalleryRepositoryImpl
import ac.dnd.bookkeeping.android.data.repository.member.MockMemberRepository
import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository
import ac.dnd.bookkeeping.android.domain.repository.FileRepository
import ac.dnd.bookkeeping.android.domain.repository.GalleryImageRepository
import ac.dnd.bookkeeping.android.domain.repository.GalleryRepository
import ac.dnd.bookkeeping.android.domain.repository.GroupRepository
import ac.dnd.bookkeeping.android.domain.repository.HeartRepository
import ac.dnd.bookkeeping.android.domain.repository.KakaoLoginRepository
Expand Down Expand Up @@ -81,4 +85,16 @@ internal abstract class RepositoryModule {
abstract fun bindsFileRepository(
fileRepository: MockFileRepository
): FileRepository

@Binds
@Singleton
abstract fun bindGalleryImageRepository(
galleryImageRepositoryImpl: GalleryImageRepositoryImpl
): GalleryImageRepository

@Binds
@Singleton
abstract fun bindGalleryRepository(
galleryRepositoryImpl: GalleryRepositoryImpl
): GalleryRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package ac.dnd.bookkeeping.android.data.remote.local.gallery

import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryFolder
import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryImage
import ac.dnd.bookkeeping.android.domain.repository.GalleryImageRepository
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.core.os.bundleOf
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject

class GalleryImageRepositoryImpl @Inject constructor(
@ApplicationContext private val context: Context
) : GalleryImageRepository {

private val uriExternal: Uri by lazy {
MediaStore.Images.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL
)
}

private val projection = arrayOf(
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns.DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns._ID
)

private val contentResolver by lazy { context.contentResolver }
private val sortedOrder = MediaStore.Images.ImageColumns.DATE_TAKEN

override fun getPhotoList(
page: Int,
loadSize: Int,
currentLocation: String?
): List<GalleryImage> {
val galleryImageList = mutableListOf<GalleryImage>()

var selection: String? = null
var selectionArgs: Array<String>? = null
if (currentLocation != null) {
selection = "${MediaStore.Images.Media.DATA} LIKE ?"
selectionArgs = arrayOf("%$currentLocation%")
}

val offset = (page - 1) * loadSize
val query = getQuery(offset, loadSize, selection, selectionArgs)

query?.use { cursor ->
while (cursor.moveToNext()) {
val id =
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID))
val name =
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DISPLAY_NAME))
val filePath =
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA))
val date =
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN))
val image = GalleryImage(
id = id,
filePath = filePath,
name = name,
date = date ?: "",
size = 0,
)
galleryImageList.add(image)
}
}
return galleryImageList
}

override fun getFolderList(): List<GalleryFolder> {
val folderList: ArrayList<GalleryFolder> = arrayListOf(GalleryFolder("최근 항목", null))
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(MediaStore.Images.Media.DATA)
val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null) {
while (cursor.moveToNext()) {
val columnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
val filePath = File(cursor.getString(columnIndex)).parent
filePath?.let {
val folder = GalleryFolder(filePath.split("/").last(), filePath)
folderList.find {
it.location == filePath
} ?: folderList.add(folder)
}
}
cursor.close()
}
return folderList
}

@SuppressLint("Recycle")
private fun getQuery(
offset: Int,
limit: Int,
selection: String?,
selectionArgs: Array<String>?,
): Cursor? {
return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
val bundle = bundleOf(
ContentResolver.QUERY_ARG_OFFSET to offset,
ContentResolver.QUERY_ARG_LIMIT to limit,
ContentResolver.QUERY_ARG_SORT_COLUMNS to arrayOf(MediaStore.Files.FileColumns.DATE_MODIFIED),
ContentResolver.QUERY_ARG_SORT_DIRECTION to ContentResolver.QUERY_SORT_DIRECTION_DESCENDING,
ContentResolver.QUERY_ARG_SQL_SELECTION to selection,
ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS to selectionArgs,
)
contentResolver.query(uriExternal, projection, bundle, null)
} else {
contentResolver.query(
uriExternal,
projection,
selection,
selectionArgs,
"$sortedOrder DESC LIMIT $limit OFFSET $offset",
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ac.dnd.bookkeeping.android.data.remote.local.gallery

import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryImage
import ac.dnd.bookkeeping.android.domain.repository.GalleryImageRepository
import androidx.paging.PagingSource
import androidx.paging.PagingState

class GalleryPagingSource(
private val imageRepository: GalleryImageRepository,
private val currentLocation: String?
) : PagingSource<Int, GalleryImage>() {

override fun getRefreshKey(state: PagingState<Int, GalleryImage>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, GalleryImage> {
return try {
val position = params.key ?: STARTING_PAGE_IDX
val data = imageRepository.getPhotoList(
page = position,
loadSize = params.loadSize,
currentLocation = currentLocation
)
val endOfPaginationReached = data.isEmpty()
val prevKey = if (position == STARTING_PAGE_IDX) null else position - 1
val nextKey =
if (endOfPaginationReached) null else position + (params.loadSize / PAGING_SIZE)
LoadResult.Page(data, prevKey, nextKey)

} catch (e: Exception) {
LoadResult.Error(e)
}
}

companion object {
const val STARTING_PAGE_IDX = 1
const val PAGING_SIZE = 30
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ac.dnd.bookkeeping.android.data.repository.gallery

import ac.dnd.bookkeeping.android.data.remote.local.gallery.GalleryPagingSource
import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryFolder
import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryImage
import ac.dnd.bookkeeping.android.domain.repository.GalleryImageRepository
import ac.dnd.bookkeeping.android.domain.repository.GalleryRepository
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class GalleryRepositoryImpl @Inject constructor(
private val galleryImageRepository: GalleryImageRepository
) : GalleryRepository {
override fun getPagingGalleryList(folder: GalleryFolder): Flow<PagingData<GalleryImage>> {
return Pager(
config = PagingConfig(
pageSize = GalleryPagingSource.PAGING_SIZE,
enablePlaceholders = true
),
pagingSourceFactory = {
GalleryPagingSource(
imageRepository = galleryImageRepository,
currentLocation = folder.location,
)
},
).flow
}

override fun getFolderList(): List<GalleryFolder> {
return galleryImageRepository.getFolderList()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ac.dnd.bookkeeping.android.domain.model.gallery

class GalleryFolder(
val name: String,
val location: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ac.dnd.bookkeeping.android.domain.model.gallery

data class GalleryImage(
val id: Long,
val filePath: String,
val name: String,
val date: String,
val size: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ac.dnd.bookkeeping.android.domain.repository

import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryFolder
import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryImage

interface GalleryImageRepository {

fun getPhotoList(
page: Int,
loadSize: Int,
currentLocation: String? = null
): List<GalleryImage>

fun getFolderList(): List<GalleryFolder>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ac.dnd.bookkeeping.android.domain.repository

import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryFolder
import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryImage
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow

interface GalleryRepository {

fun getPagingGalleryList(
folder: GalleryFolder
): Flow<PagingData<GalleryImage>>

fun getFolderList(): List<GalleryFolder>

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ac.dnd.bookkeeping.android.domain.usecase.gallery

import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryFolder
import ac.dnd.bookkeeping.android.domain.repository.GalleryRepository
import javax.inject.Inject

class GetFolderListUseCase @Inject constructor(
private val galleryRepository: GalleryRepository
) {
operator fun invoke(): List<GalleryFolder> {
return galleryRepository.getFolderList()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package ac.dnd.bookkeeping.android.domain.usecase.gallery

import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryFolder
import ac.dnd.bookkeeping.android.domain.model.gallery.GalleryImage
import ac.dnd.bookkeeping.android.domain.repository.GalleryRepository
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class GetPhotoListUseCase @Inject constructor(
private val galleryRepository: GalleryRepository
) {
operator fun invoke(
currentFolder: GalleryFolder
): Flow<PagingData<GalleryImage>> {
return galleryRepository.getPagingGalleryList(currentFolder)
}
}
9 changes: 5 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ hilt-compose = "1.1.0"
androidx-core = "1.12.0"
androidx-appcompat = "1.6.1"
androidx-room = "2.6.1"
androidx-paging = "3.1.1"
androidx-paging = "3.2.1"
androidx-compose = "1.5.4"
androidx-compose-navigation = "2.7.0"
androidx-compose-lifecycle = "2.7.0"
Expand All @@ -35,7 +35,7 @@ kakao = "2.12.1"
ktor = "2.3.7"
kotlinx-serialization = "1.6.1"
# UI
coil-compose = "1.3.2"
coil-compose = "2.4.0"
lottie = "5.2.0"
holix-bottomsheet-compose = "1.3.1"
# Logging
Expand Down Expand Up @@ -65,6 +65,7 @@ androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androi
androidx-compose-ui-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "androidx-compose" }
androidx-compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-compose-navigation" }
androidx-compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-compose-lifecycle" }
androidx-compose-paging = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" }
# AndroidX Data
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" }
Expand All @@ -74,7 +75,7 @@ androidx-paging-common = { module = "androidx.paging:paging-common-ktx", version
# Google
google-system-contoller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "google-system-contoller" }
# Kakao
kakao-user = { module = "com.kakao.sdk:v2-user", version.ref = "kakao"}
kakao-user = { module = "com.kakao.sdk:v2-user", version.ref = "kakao" }
# Network
ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
Expand Down Expand Up @@ -112,7 +113,7 @@ sentry = { id = "io.sentry.android.gradle", version.ref = "plugin-sentry" }
kotlin = ["kotlin", "kotlinx-coroutines-android", "kotlinx-coroutines-core", "kotlinx-datetime"]
androidx-data = ["androidx-room-runtime", "androidx-room-coroutine", "androidx-paging-runtime"]
androidx-presentation = ["androidx-core", "androidx-appcompat", "androidx-compose-material", "androidx-compose-ui",
"androidx-compose-ui-preview", "androidx-compose-navigation", "androidx-compose-lifecycle"]
"androidx-compose-ui-preview", "androidx-compose-navigation", "androidx-compose-lifecycle", "coil-compose"]
network = ["ktor-core", "ktor-okhttp", "ktor-resources", "ktor-content-negotiation", "ktor-kotlinx-serialization",
"ktor-auth", "kotlinx-serialization"]
logging = ["timber", "sentry"]
2 changes: 2 additions & 0 deletions presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ dependencies {

debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)

implementation(libs.androidx.compose.paging)
}

fun getLocalProperty(propertyKey: String): String {
Expand Down
Loading

0 comments on commit 778c6fe

Please sign in to comment.