Skip to content

Commit

Permalink
Merge pull request #11 from wawandco/camera-feat
Browse files Browse the repository at this point in the history
Camera feat 📷
  • Loading branch information
joeariasc authored Jan 9, 2025
2 parents 4859b78 + 953a1ad commit 378275b
Show file tree
Hide file tree
Showing 70 changed files with 2,373 additions and 220 deletions.
10 changes: 10 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,14 @@ dependencies {
**** Maps SDK
****************************************************** */
implementation(libs.play.services.map)

/* *****************************************************
**** Accompanist Permissions
****************************************************** */
implementation(libs.accompanist.permissions)

/* *****************************************************
**** CameraX
****************************************************** */
implementation(libs.bundles.androidx.camera)
}
12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:resizeable="true"
android:smallScreens="false" />

<!-- Permissions-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />

<uses-feature
android:name="android.hardware.camera.any"
android:required="false" />

<application
android:name=".App"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import co.wawand.composetypesafenavigation.data.local.database.entity.UserEntity
AuthorEntity::class,
PostEntity::class,
AlbumEntity::class,
PhotoEntity::class
PhotoEntity::class,
],
version = 1,
exportSchema = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
import co.wawand.composetypesafenavigation.data.local.database.entity.PhotoEntity
import co.wawand.composetypesafenavigation.data.local.database.entity.PhotoWithAlbum

@Dao
interface PhotoDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
@Upsert
suspend fun upsertUserPhoto(userPhoto: PhotoEntity): Long

@Transaction
@Query("SELECT * FROM photos where userId = :id")
suspend fun getPhotosByUserId(id: Long): List<PhotoEntity>

@Upsert
suspend fun insertPhotos(photos: List<PhotoEntity>)

@Transaction
Expand All @@ -20,6 +28,17 @@ interface PhotoDao {

@Transaction
@Query("SELECT * FROM photos where id = :id")
suspend fun getPhotoById(id: Long): PhotoWithAlbum
suspend fun getPhotoWithAlbumById(id: Long): PhotoWithAlbum

@Transaction
@Query("SELECT * FROM photos where id = :id")
suspend fun getPhotoById(id: Long): PhotoEntity

@Transaction
@Query("SELECT path FROM photos where id = :id")
suspend fun getPhotoPathById(id: Long): String

@Transaction
@Query("DELETE FROM photos where id = :id")
suspend fun deletePhotoById(id: Long)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,25 @@ import androidx.room.Relation
data class PhotoEntity(
@PrimaryKey(autoGenerate = true) val id: Long,
val title: String,
val thumbnailUrl: String,
val url: String,
val albumId: Long,
val type: PhotoType,

// Fields specific to remote photos
val thumbnailUrl: String? = null,
val url: String? = null,
val albumId: Long? = null,

// Fields specific to local photos
val path: String? = null,
val size: Long? = null,
val lastModified: Long? = null,
val userId: Long? = null,
)

enum class PhotoType {
LOCAL,
REMOTE
}

data class PhotoWithAlbum(
@Embedded
val photoEntity: PhotoEntity,
Expand All @@ -22,5 +36,5 @@ data class PhotoWithAlbum(
parentColumn = "albumId",
entityColumn = "id"
)
val albumEntity: AlbumEntity
val albumEntity: AlbumEntity?
)
Original file line number Diff line number Diff line change
@@ -1,38 +1,73 @@
package co.wawand.composetypesafenavigation.data.mapper

import co.wawand.composetypesafenavigation.data.local.database.entity.PhotoEntity
import co.wawand.composetypesafenavigation.data.local.database.entity.PhotoType
import co.wawand.composetypesafenavigation.data.local.database.entity.PhotoWithAlbum
import co.wawand.composetypesafenavigation.data.remote.api.entity.PhotoAPIEntity
import co.wawand.composetypesafenavigation.domain.model.Photo
import co.wawand.composetypesafenavigation.domain.model.BasePhoto
import co.wawand.composetypesafenavigation.domain.model.LocalPhoto
import co.wawand.composetypesafenavigation.domain.model.RemotePhoto


fun PhotoAPIEntity.toDBEntity(): PhotoEntity {
val randomNumber = (0..100000).random()
return PhotoEntity(
id = id,
title = title,
type = PhotoType.REMOTE,
thumbnailUrl = "https://picsum.photos/seed/$randomNumber/256/256",
url = "https://picsum.photos/seed/$randomNumber/2400/1600",
albumId = albumId
)
}

fun PhotoEntity.toDomain(): Photo = Photo(
id = id,
title = title,
thumbnailUrl = thumbnailUrl,
url = url,
album = albumId.toString()
)
fun PhotoEntity.toDomain(): BasePhoto {
return when (type) {
PhotoType.LOCAL -> LocalPhoto(
id = id,
title = title,
path = path!!,
size = size!!,
lastModified = lastModified!!
)

fun PhotoWithAlbum.toDomain(): Photo {
PhotoType.REMOTE -> RemotePhoto(
id = id,
title = title,
thumbnailUrl = thumbnailUrl!!,
url = url!!,
album = albumId.toString()
)
}
}

fun PhotoWithAlbum.toDomain(): RemotePhoto {
val photo = this.photoEntity
val album = this.albumEntity

return Photo(
return RemotePhoto(
id = photo.id,
title = photo.title,
thumbnailUrl = photo.thumbnailUrl,
url = photo.url,
album = album.title
thumbnailUrl = photo.thumbnailUrl!!,
url = photo.url!!,
album = album?.title ?: ""
)
}
}

fun PhotoEntity.toPhotoDetailsEntity(): LocalPhoto = LocalPhoto(
id = id,
title = title,
path = path!!,
size = size!!,
lastModified = lastModified!!
)

fun LocalPhoto.toEntity(userId: Long): PhotoEntity = PhotoEntity(
id = id,
title = title,
type = PhotoType.LOCAL,
path = path,
size = size,
lastModified = lastModified,
userId = userId,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package co.wawand.composetypesafenavigation.data.repository

import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import co.wawand.composetypesafenavigation.core.Constant.GENERIC_ERROR
import co.wawand.composetypesafenavigation.core.util.Resource
import co.wawand.composetypesafenavigation.di.IoDispatcher
import co.wawand.composetypesafenavigation.domain.repository.CameraRepository
import co.wawand.composetypesafenavigation.domain.service.FileService
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

class CameraRepositoryImpl @Inject constructor(
private val fileService: FileService,
@IoDispatcher private val dispatcher: CoroutineDispatcher
) : CameraRepository {

override suspend fun capturePhoto(
prefix: String,
executor: Executor,
imageCapture: ImageCapture
): Flow<Resource<String>> =
flow {
try {
val photoFile = fileService.generatePhotoFile(prefix)

suspendCancellableCoroutine { continuation ->
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

imageCapture.takePicture(
outputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
continuation.resume(photoFile.absolutePath)
}

override fun onError(exception: ImageCaptureException) {
continuation.resumeWithException(exception)
}
}
)
}.let { uri ->
emit(Resource.Success(uri))
}
} catch (e: Exception) {
emit(Resource.Error(e.message ?: GENERIC_ERROR))
}
}.flowOn(dispatcher)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,27 @@ package co.wawand.composetypesafenavigation.data.repository
import co.wawand.composetypesafenavigation.core.Constant.GENERIC_ERROR
import co.wawand.composetypesafenavigation.core.util.Resource
import co.wawand.composetypesafenavigation.data.local.database.dao.PhotoDao
import co.wawand.composetypesafenavigation.data.local.database.entity.PhotoType
import co.wawand.composetypesafenavigation.data.mapper.toDomain
import co.wawand.composetypesafenavigation.domain.model.Photo
import co.wawand.composetypesafenavigation.data.mapper.toEntity
import co.wawand.composetypesafenavigation.data.mapper.toPhotoDetailsEntity
import co.wawand.composetypesafenavigation.domain.model.LocalPhoto
import co.wawand.composetypesafenavigation.domain.model.RemotePhoto
import co.wawand.composetypesafenavigation.domain.repository.PhotoRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.File
import javax.inject.Inject

class PhotoRepositoryImpl @Inject constructor(
private val photoDao: PhotoDao
) : PhotoRepository {
override suspend fun getPhotoDetails(id: Long): Flow<Resource<Photo>> = flow {

override suspend fun getRemotePhotoDetails(id: Long): Flow<Resource<RemotePhoto>> = flow {
emit(Resource.Loading())

runCatching {
photoDao.getPhotoById(id)
photoDao.getPhotoWithAlbumById(id)
}.onFailure {
it.printStackTrace()
emit(Resource.Error(it.message ?: GENERIC_ERROR))
Expand All @@ -26,4 +32,83 @@ class PhotoRepositoryImpl @Inject constructor(
}
}

override suspend fun getLocalPhotoDetails(id: Long): Flow<Resource<LocalPhoto>> = flow {
emit(Resource.Loading())

runCatching {
photoDao.getPhotoById(id)
}.onFailure {
it.printStackTrace()
emit(Resource.Error(it.message ?: GENERIC_ERROR))
}.onSuccess {
emit(Resource.Success(it.toPhotoDetailsEntity()))
}
}

override suspend fun persistUserPhoto(
photo: LocalPhoto,
userId: Long
): Flow<Resource<Long>> = flow {
emit(Resource.Loading())

runCatching {
photoDao.upsertUserPhoto(photo.toEntity(userId))
}.onFailure {
it.printStackTrace()
emit(Resource.Error(it.message ?: GENERIC_ERROR))
}.onSuccess {
emit(Resource.Success(it))
}
}

override suspend fun getUserPhotos(userId: Long): Flow<Resource<List<LocalPhoto>>> =
flow {
emit(Resource.Loading())

runCatching {
photoDao.getPhotosByUserId(userId)
}.onFailure {
it.printStackTrace()
emit(Resource.Error(it.message ?: GENERIC_ERROR))
}.onSuccess {
emit(Resource.Success(it.map { photo -> photo.toPhotoDetailsEntity() }))
}
}

override suspend fun getPhotoPathById(photoId: Long): Flow<Resource<String>> = flow {
emit(Resource.Loading())

runCatching {
photoDao.getPhotoPathById(photoId)
}.onFailure {
it.printStackTrace()
emit(Resource.Error(it.message ?: GENERIC_ERROR))
}.onSuccess {
emit(Resource.Success(it))
}
}

override suspend fun deleteUserPhoto(photoId: Long): Flow<Resource<Boolean>> = flow {
emit(Resource.Loading())

runCatching {
val photo = photoDao.getPhotoById(photoId)

if (photo.type == PhotoType.LOCAL) {
val file = File(photo.path!!)
val success = file.delete()
if (!success) {
emit(Resource.Error("Error deleting file"))
return@flow
}
}
photoDao.deletePhotoById(photoId)
}.onFailure {
it.printStackTrace()
emit(Resource.Error(it.message ?: GENERIC_ERROR))
}.onSuccess {
emit(Resource.Success(true))
}
}

}
Loading

0 comments on commit 378275b

Please sign in to comment.