From 17aff3533b77e3689359979b34b169ac7d6339fa Mon Sep 17 00:00:00 2001 From: Sanjay Prajapati <82382452+sanjay-mi@users.noreply.github.com> Date: Mon, 18 Jul 2022 18:29:58 +0530 Subject: [PATCH] Develop (#62) * -Enabled actual circular crop -Optimized the attributes * Upgrade lib version and README.md * Updated dependencies versions, Minor changes in style (#18) - Used ViewModelProvider constructor to build the ViewModel instead of ViewModelProviders * Update library version and README.md * Added file size limit restriction to display in grid * Optional crop (#23) Make single image selection cropping optional * Update library version and README.md * Update README.md * Feature/latest version support (#26) * Latest Android 11 support - Gradle version updated to v4.1.3 instead of v4.1.1 - Kotlin version updated to v1.4.32 instead of v1.4.21 - Multidex added - CompileSdkVersion updated to android-S instead of 30 - buildToolVersion 30.0.3 added - ViewModelProviders code changes due to deprecation - request permission and start activity for result code changes in docsFragment and FolderFragment * PDF and other non media files not listing issue solved for Android 11 and 12 * Camera not working issue resolved and Camera permission changes applied * Android 10 Non media file choose from system file explorer feature implementation - Gradle version updated to v4.2.0 instead of 4.1.3 - Kotlin version updated to v1.5.0 instead of 1.4.32 * Lassi picker version updated to v0.2.0 instead of v0.1.7 * CompileSdkVersion changed to 30 instead of android-S due to APK file not installed issue - BuildToolVersion changed to v30.0.3 instead of v31.0.0 rc3 * Cropping image URI not working issue resolved * Feature/latest version support (#28) * Added file size limit restriction to display in grid (#22) * -Enabled actual circular crop -Optimized the attributes * Upgrade lib version and README.md * Updated dependencies versions, Minor changes in style (#18) - Used ViewModelProvider constructor to build the ViewModel instead of ViewModelProviders * Update library version and README.md * Added file size limit restriction to display in grid Co-authored-by: Bhoomi Shah Co-authored-by: AKASH PATEL Co-authored-by: milanvadhel-mi <73939866+milanvadhel-mi@users.noreply.github.com> * Optional crop (#23) Make single image selection cropping optional * Update library version and README.md * Update README.md * Latest Android 11 support [WIP] - Gradle version updated to v4.1.3 instead of v4.1.1 - Kotlin version updated to v1.4.32 instead of v1.4.21 - Multidex added - CompileSdkVersion updated to android-S instead of 30 - buildToolVersion 30.0.3 added - ViewModelProviders code changes due to deprecation - request permission and start activity for result code changes in docsFragment and FolderFragment * PDF and other non media files not listing issue solved for Android 11 and 12 * Camera not working issue resolved and Camera permission changes applied * Android 10 Non media file choose from system file explorer feature implementation - Gradle version updated to v4.2.0 instead of 4.1.3 - Kotlin version updated to v1.5.0 instead of 1.4.32 * Lassi picker version updated to v0.2.0 instead of v0.1.7 * CompileSdkVersion changed to 30 instead of android-S due to APK file not installed issue - BuildToolVersion changed to v30.0.3 instead of v31.0.0 rc3 * Cropping image URI not working issue resolved Co-authored-by: Malik Motani Co-authored-by: Bhoomi Shah Co-authored-by: AKASH PATEL Co-authored-by: milanvadhel-mi <73939866+milanvadhel-mi@users.noreply.github.com> Co-authored-by: Faiyaz meghreji <50236417+faiyaz92@users.noreply.github.com> * - Resolved albumId exception in audio picker (#36) - Added selectionDrawable attribute option in xml - Updated UI - Updated library version and plugins - Updated README.md * Update README.md * fixed crash on reject permission. (#48) * ANR issue resolved for Folder listing * Library version updated to 0.3.0 instead of 0.2.2 - target sdk version updated to 32 * Feature/doc enhancement (#54) * Develop (#52) - ANR issue was resolved. - Reject permission crash issue resolved. - Library version updated to 0.3.0 - Target SDK version updated, Android 12 support. * - System default view support given with defined view type - Min SDK version updated to 19 - Lassi version updated to v0.4.0 Co-authored-by: milanvadhel-mi <73939866+milanvadhel-mi@users.noreply.github.com> Co-authored-by: Chirag Prajapati Co-authored-by: Nkgohil007 <89060719+Nkgohil007@users.noreply.github.com> Co-authored-by: AKASH PATEL * Bug fixes (#59) Co-authored-by: Akash Patel * - version upgrade * - version upgrade to 1.0.0 from 0.5.0 Co-authored-by: Bhoomi Shah Co-authored-by: AKASH PATEL Co-authored-by: Malik Motani Co-authored-by: milanvadhel-mi <73939866+milanvadhel-mi@users.noreply.github.com> Co-authored-by: Faiyaz meghreji <50236417+faiyaz92@users.noreply.github.com> Co-authored-by: Chirag Prajapati Co-authored-by: Chirag Prajapati <87471481+chiragmi@users.noreply.github.com> Co-authored-by: Nkgohil007 <89060719+Nkgohil007@users.noreply.github.com> Co-authored-by: AKASH PATEL Co-authored-by: vrajendraBhavsar <84775226+vrajendraBhavsar@users.noreply.github.com> Co-authored-by: Vrajendra --- .../main/java/com/lassi/app/MainActivity.kt | 2 +- .../lassi/app/adapter/SelectedMediaAdapter.kt | 1 - lassi/build.gradle | 15 +- .../lassi/common/extenstions/LiveDataExt.kt | 22 + .../java/com/lassi/common/utils/ImageUtils.kt | 2 +- .../lassi/data/database/MediaFileDatabase.kt | 37 ++ .../java/com/lassi/data/media/MiItemMedia.kt | 11 + .../data/media/entity/AlbumCoverPathEntity.kt | 39 ++ .../lassi/data/media/entity/DurationEntity.kt | 40 ++ .../lassi/data/media/entity/MediaFileDao.kt | 66 +++ .../data/media/entity/MediaFileEntity.kt | 49 +++ .../data/media/entity/SelectedMediaModel.kt | 21 + .../{ => repository}/MediaRepositoryImpl.kt | 393 +++++++++++++----- .../repository/SelectedMediaRepositoryImpl.kt | 82 ++++ .../lassi/data/mediadirectory/FolderBucket.kt | 12 + .../com/lassi/domain/media/MediaRepository.kt | 7 +- .../domain/media/SelectedMediaRepository.kt | 9 + .../presentation/docs/DocsViewModelFactory.kt | 2 +- .../lassi/presentation/media/MediaFragment.kt | 49 ++- .../media/SelectedMediaViewModel.kt | 31 +- .../media/adapter/MediaAdapter.kt | 1 - .../mediadirectory/FolderFragment.kt | 64 +-- .../mediadirectory/FolderViewModel.kt | 102 ++++- .../mediadirectory/FolderViewModelFactory.kt | 2 +- .../LassiMediaPickerActivity.kt | 9 +- .../SelectedMediaViewModelFactory.kt | 18 + .../mediadirectory/adapter/FolderAdapter.kt | 37 +- .../videopreview/VideoPreviewActivity.kt | 3 +- .../main/res/layout/fragment_media_picker.xml | 14 + lassi/src/main/res/values/colors.xml | 1 + lassi/src/main/res/values/strings.xml | 3 + 31 files changed, 939 insertions(+), 205 deletions(-) create mode 100644 lassi/src/main/java/com/lassi/common/extenstions/LiveDataExt.kt create mode 100644 lassi/src/main/java/com/lassi/data/database/MediaFileDatabase.kt create mode 100644 lassi/src/main/java/com/lassi/data/media/MiItemMedia.kt create mode 100644 lassi/src/main/java/com/lassi/data/media/entity/AlbumCoverPathEntity.kt create mode 100644 lassi/src/main/java/com/lassi/data/media/entity/DurationEntity.kt create mode 100644 lassi/src/main/java/com/lassi/data/media/entity/MediaFileDao.kt create mode 100644 lassi/src/main/java/com/lassi/data/media/entity/MediaFileEntity.kt create mode 100644 lassi/src/main/java/com/lassi/data/media/entity/SelectedMediaModel.kt rename lassi/src/main/java/com/lassi/data/media/{ => repository}/MediaRepositoryImpl.kt (52%) create mode 100644 lassi/src/main/java/com/lassi/data/media/repository/SelectedMediaRepositoryImpl.kt create mode 100644 lassi/src/main/java/com/lassi/data/mediadirectory/FolderBucket.kt create mode 100644 lassi/src/main/java/com/lassi/domain/media/SelectedMediaRepository.kt create mode 100644 lassi/src/main/java/com/lassi/presentation/mediadirectory/SelectedMediaViewModelFactory.kt diff --git a/app/src/main/java/com/lassi/app/MainActivity.kt b/app/src/main/java/com/lassi/app/MainActivity.kt index f5cd489..9f8b213 100644 --- a/app/src/main/java/com/lassi/app/MainActivity.kt +++ b/app/src/main/java/com/lassi/app/MainActivity.kt @@ -60,7 +60,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener { .setCropAspectRatio(1, 1) .setCompressionRation(10) .setMinFileSize(0) - .setMaxFileSize(1000) + .setMaxFileSize(10000) .enableActualCircleCrop() .setSupportedFileTypes("jpg", "jpeg", "png", "webp", "gif") .enableFlip() diff --git a/app/src/main/java/com/lassi/app/adapter/SelectedMediaAdapter.kt b/app/src/main/java/com/lassi/app/adapter/SelectedMediaAdapter.kt index dfc6ca7..efdc416 100644 --- a/app/src/main/java/com/lassi/app/adapter/SelectedMediaAdapter.kt +++ b/app/src/main/java/com/lassi/app/adapter/SelectedMediaAdapter.kt @@ -9,7 +9,6 @@ import com.lassi.common.extenstions.loadImage import com.lassi.common.utils.ImageUtils import com.lassi.data.media.MiMedia import kotlinx.android.synthetic.main.row_selected_media.view.* -import java.util.* class SelectedMediaAdapter(private val onItemClicked: (miMedia: MiMedia) -> Unit) : RecyclerView.Adapter() { diff --git a/lassi/build.gradle b/lassi/build.gradle index f021293..cafd920 100644 --- a/lassi/build.gradle +++ b/lassi/build.gradle @@ -12,8 +12,8 @@ android { defaultConfig { minSdkVersion 19 targetSdkVersion 32 - versionCode 21 - versionName "0.4.0" + versionCode 22 + versionName "1.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true multiDexEnabled true @@ -72,6 +72,17 @@ dependencies { implementation 'androidx.fragment:fragment-ktx:1.4.1' implementation "androidx.multidex:multidex:2.0.1" + + // Room DB + implementation 'androidx.room:room-runtime:2.4.2' + kapt 'androidx.room:room-compiler:2.4.2' + implementation 'androidx.room:room-ktx:2.4.2' + + //Coroutine + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' + + //Gson + implementation 'com.google.code.gson:gson:2.8.8' } repositories { mavenCentral() diff --git a/lassi/src/main/java/com/lassi/common/extenstions/LiveDataExt.kt b/lassi/src/main/java/com/lassi/common/extenstions/LiveDataExt.kt new file mode 100644 index 0000000..c3c5989 --- /dev/null +++ b/lassi/src/main/java/com/lassi/common/extenstions/LiveDataExt.kt @@ -0,0 +1,22 @@ +package com.lassi.common.extenstions + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.lassi.common.utils.Logger +import com.lassi.data.common.Response + +fun LiveData.safeObserve(owner: LifecycleOwner, observer: (T) -> Unit) { + observe(owner) { it?.let(observer) ?: Logger.d("TAG", "Live data value is null") } +} + +fun MutableLiveData>.setSuccess(data: T) = postValue(Response.Success(data)) + +fun MutableLiveData>.setLoading() = postValue(Response.Loading()) + +fun MutableLiveData>.setError(throwable: Throwable) = + postValue(Response.Error(throwable)) + +fun MutableLiveData>.isLoading() = value is Response.Loading + +fun LiveData>.isLoading() = value is Response.Loading diff --git a/lassi/src/main/java/com/lassi/common/utils/ImageUtils.kt b/lassi/src/main/java/com/lassi/common/utils/ImageUtils.kt index 30a487b..e4cdc82 100644 --- a/lassi/src/main/java/com/lassi/common/utils/ImageUtils.kt +++ b/lassi/src/main/java/com/lassi/common/utils/ImageUtils.kt @@ -12,4 +12,4 @@ object ImageUtils { miMedia.path } } -} \ No newline at end of file +} diff --git a/lassi/src/main/java/com/lassi/data/database/MediaFileDatabase.kt b/lassi/src/main/java/com/lassi/data/database/MediaFileDatabase.kt new file mode 100644 index 0000000..1582f99 --- /dev/null +++ b/lassi/src/main/java/com/lassi/data/database/MediaFileDatabase.kt @@ -0,0 +1,37 @@ +package com.lassi.data.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.lassi.data.media.entity.AlbumCoverPathEntity +import com.lassi.data.media.entity.DurationEntity +import com.lassi.data.media.entity.MediaFileDao +import com.lassi.data.media.entity.MediaFileEntity + +@Database( + entities = arrayOf( + MediaFileEntity::class, + DurationEntity::class, + AlbumCoverPathEntity::class + ), version = 1, exportSchema = false +) +abstract class MediaFileDatabase : RoomDatabase() { + abstract fun mediaFileDao(): MediaFileDao + + companion object { + @Volatile + private var INSTANCE: MediaFileDatabase? = null + private val LOCK = Any() + + operator fun invoke(context: Context) = INSTANCE ?: synchronized(LOCK) { + buildDatabase(context).also { + INSTANCE = it + } + } + + private fun buildDatabase(context: Context): MediaFileDatabase = Room.databaseBuilder( + context, MediaFileDatabase::class.java, "media_file_database" + ).build() + } +} \ No newline at end of file diff --git a/lassi/src/main/java/com/lassi/data/media/MiItemMedia.kt b/lassi/src/main/java/com/lassi/data/media/MiItemMedia.kt new file mode 100644 index 0000000..78910c5 --- /dev/null +++ b/lassi/src/main/java/com/lassi/data/media/MiItemMedia.kt @@ -0,0 +1,11 @@ +package com.lassi.data.media + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class MiItemMedia( + var bucketName: String? = null, + var latestItemPathForBucket: String? = null, + var totalItemSizeForBucket: Long = 0L +) : Parcelable diff --git a/lassi/src/main/java/com/lassi/data/media/entity/AlbumCoverPathEntity.kt b/lassi/src/main/java/com/lassi/data/media/entity/AlbumCoverPathEntity.kt new file mode 100644 index 0000000..14ade9e --- /dev/null +++ b/lassi/src/main/java/com/lassi/data/media/entity/AlbumCoverPathEntity.kt @@ -0,0 +1,39 @@ +package com.lassi.data.media.entity + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.lassi.data.media.entity.AlbumCoverPathEntity.Companion.ALBUM_COVER_ENTITY +import com.lassi.data.media.entity.AlbumCoverPathEntity.Companion.ALBUM_COVER_MEDIA_ID +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_ID +import kotlinx.android.parcel.Parcelize + +@Parcelize +@Entity( + tableName = ALBUM_COVER_ENTITY, + foreignKeys = arrayOf( + ForeignKey( + entity = MediaFileEntity::class, + parentColumns = arrayOf(MEDIA_ID), + childColumns = arrayOf(ALBUM_COVER_MEDIA_ID), + onDelete = ForeignKey.CASCADE + ) + ) +) +data class AlbumCoverPathEntity( + @PrimaryKey + @ColumnInfo(name = ALBUM_COVER_MEDIA_ID) + var mediaId: Long, + + @ColumnInfo(name = ALBUM_COVER_MEDIA_PATH, defaultValue = "default_media_album_cover_path") + var mediaAlbumCoverPath: String, + + ) : Parcelable { + companion object { + const val ALBUM_COVER_ENTITY = "album_cover" + const val ALBUM_COVER_MEDIA_ID = "album_cover_media_id" + const val ALBUM_COVER_MEDIA_PATH = "album_cover_path" + } +} \ No newline at end of file diff --git a/lassi/src/main/java/com/lassi/data/media/entity/DurationEntity.kt b/lassi/src/main/java/com/lassi/data/media/entity/DurationEntity.kt new file mode 100644 index 0000000..f5e3c86 --- /dev/null +++ b/lassi/src/main/java/com/lassi/data/media/entity/DurationEntity.kt @@ -0,0 +1,40 @@ +package com.lassi.data.media.entity + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.CASCADE +import androidx.room.PrimaryKey +import com.lassi.data.media.entity.DurationEntity.Companion.DURATION_ENTITY +import com.lassi.data.media.entity.DurationEntity.Companion.DURATION_MEDIA_ID +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_ID +import kotlinx.android.parcel.Parcelize + +@Parcelize +@Entity( + tableName = DURATION_ENTITY, + foreignKeys = arrayOf( + ForeignKey( + entity = MediaFileEntity::class, + parentColumns = arrayOf(MEDIA_ID), + childColumns = arrayOf(DURATION_MEDIA_ID), + onDelete = CASCADE + ) + ) +) +data class DurationEntity( + @PrimaryKey + @ColumnInfo(name = DURATION_MEDIA_ID) + var mediaId: Long, + + @ColumnInfo(name = DURATION_MEDIA_DURATION, defaultValue = "default_media_duration") + var mediaDuration: Long, + + ) : Parcelable { + companion object { + const val DURATION_ENTITY = "duration" + const val DURATION_MEDIA_ID = "duration_media_id" + const val DURATION_MEDIA_DURATION = "media_duration" + } +} \ No newline at end of file diff --git a/lassi/src/main/java/com/lassi/data/media/entity/MediaFileDao.kt b/lassi/src/main/java/com/lassi/data/media/entity/MediaFileDao.kt new file mode 100644 index 0000000..a4b6915 --- /dev/null +++ b/lassi/src/main/java/com/lassi/data/media/entity/MediaFileDao.kt @@ -0,0 +1,66 @@ +package com.lassi.data.media.entity + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.lassi.data.media.entity.AlbumCoverPathEntity.Companion.ALBUM_COVER_ENTITY +import com.lassi.data.media.entity.AlbumCoverPathEntity.Companion.ALBUM_COVER_MEDIA_ID +import com.lassi.data.media.entity.AlbumCoverPathEntity.Companion.ALBUM_COVER_MEDIA_PATH +import com.lassi.data.media.entity.DurationEntity.Companion.DURATION_ENTITY +import com.lassi.data.media.entity.DurationEntity.Companion.DURATION_MEDIA_DURATION +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_BUCKET +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_DATE_ADDED +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_FILE_ENTITY +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_ID +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_NAME +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_PATH +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_SIZE +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_TYPE +import kotlinx.coroutines.flow.Flow + +@Dao +interface MediaFileDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertMediaFile(mediaFileEntity: MediaFileEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertDuration(durationEntity: DurationEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAlbumCover(albumCoverPathEntity: AlbumCoverPathEntity) + + @Query("SELECT MAX($MEDIA_DATE_ADDED) as LargestDate FROM $MEDIA_FILE_ENTITY WHERE $MEDIA_TYPE = :mediaType") + suspend fun getMaxDateFromMediaTable(mediaType: Int): Long + + @Query("SELECT COUNT(*) == 0 FROM $MEDIA_FILE_ENTITY WHERE $MEDIA_TYPE = :mediaType") + suspend fun getDataCount(mediaType: Int): Boolean + + @Query("SELECT DISTINCT $MEDIA_BUCKET FROM $MEDIA_FILE_ENTITY WHERE $MEDIA_TYPE = :mediaType") + fun getDistinctBucketList(mediaType: Int): Flow> + + @Query("SELECT MAX($MEDIA_DATE_ADDED) as LargestDate FROM $MEDIA_FILE_ENTITY WHERE $MEDIA_BUCKET = :bucket") + suspend fun getLatestDateForBucket(bucket: String): Long + + @Query("SELECT $MEDIA_PATH as LatestItem FROM $MEDIA_FILE_ENTITY WHERE $MEDIA_BUCKET = :bucket AND $MEDIA_TYPE = :mediaType AND $MEDIA_DATE_ADDED = (SELECT MAX($MEDIA_DATE_ADDED) as LargestDate FROM $MEDIA_FILE_ENTITY WHERE $MEDIA_BUCKET = :bucket)") + suspend fun getLatestItemForBucket(bucket: String, mediaType: Int): String + + @Query("SELECT COUNT($MEDIA_ID) FROM $MEDIA_FILE_ENTITY WHERE $MEDIA_BUCKET = :bucket AND $MEDIA_TYPE = :mediaType") + suspend fun getTotalItemSizeForBucket(bucket: String, mediaType: Int): Long + + @Query( + "SELECT $MEDIA_FILE_ENTITY.$MEDIA_ID as mediaId, $MEDIA_FILE_ENTITY.$MEDIA_NAME as mediaName, " + + "$MEDIA_FILE_ENTITY.$MEDIA_PATH as mediaPath, $MEDIA_FILE_ENTITY.$MEDIA_SIZE as mediaSize, $DURATION_ENTITY.$DURATION_MEDIA_DURATION as mediaDuration, $ALBUM_COVER_ENTITY.$ALBUM_COVER_MEDIA_PATH as mediaAlbumCoverPath" + + " FROM $MEDIA_FILE_ENTITY" + + " INNER JOIN $DURATION_ENTITY" + + " ON $MEDIA_FILE_ENTITY.$MEDIA_ID = $DURATION_ENTITY.duration_media_id" + + " INNER JOIN $ALBUM_COVER_ENTITY" + + " ON $MEDIA_FILE_ENTITY.$MEDIA_ID = $ALBUM_COVER_ENTITY.$ALBUM_COVER_MEDIA_ID" + + " WHERE $MEDIA_BUCKET = :bucket AND $MEDIA_TYPE = :mediaType" + ) + fun getSelectedMediaFile(bucket: String, mediaType: Int): List + + @Query("SELECT * FROM $MEDIA_FILE_ENTITY WHERE $MEDIA_BUCKET = :bucket AND $MEDIA_TYPE = :mediaType") + fun getSelectedImageMediaFile(bucket: String, mediaType: Int): List +} \ No newline at end of file diff --git a/lassi/src/main/java/com/lassi/data/media/entity/MediaFileEntity.kt b/lassi/src/main/java/com/lassi/data/media/entity/MediaFileEntity.kt new file mode 100644 index 0000000..2563091 --- /dev/null +++ b/lassi/src/main/java/com/lassi/data/media/entity/MediaFileEntity.kt @@ -0,0 +1,49 @@ +package com.lassi.data.media.entity + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_FILE_ENTITY +import com.lassi.data.media.entity.MediaFileEntity.Companion.MEDIA_ID +import kotlinx.android.parcel.Parcelize + +@Parcelize +@Entity(tableName = MEDIA_FILE_ENTITY, indices = [Index(value = [MEDIA_ID], unique = true)]) +data class MediaFileEntity( + + @PrimaryKey + @ColumnInfo(name = MEDIA_ID) + var mediaId: Long, + + @ColumnInfo(name = MEDIA_NAME) + var mediaName: String, + + @ColumnInfo(name = MEDIA_PATH) + var mediaPath: String, + + @ColumnInfo(name = MEDIA_BUCKET, defaultValue = "default_media_bucket") + var mediaBucket: String, + + @ColumnInfo(name = MEDIA_SIZE) + var mediaSize: Long, + + @ColumnInfo(name = MEDIA_DATE_ADDED) + var mediaDateAdded: Long, + + @ColumnInfo(name = MEDIA_TYPE) + var mediaType: Int, + +) : Parcelable { + companion object { + const val MEDIA_FILE_ENTITY = "media" + const val MEDIA_ID = "media_id" + const val MEDIA_NAME = "media_name" + const val MEDIA_PATH = "media_path" + const val MEDIA_BUCKET = "media_bucket" + const val MEDIA_SIZE = "media_size" + const val MEDIA_TYPE = "media_type" + const val MEDIA_DATE_ADDED = "media_date_added" + } +} diff --git a/lassi/src/main/java/com/lassi/data/media/entity/SelectedMediaModel.kt b/lassi/src/main/java/com/lassi/data/media/entity/SelectedMediaModel.kt new file mode 100644 index 0000000..3738d40 --- /dev/null +++ b/lassi/src/main/java/com/lassi/data/media/entity/SelectedMediaModel.kt @@ -0,0 +1,21 @@ +package com.lassi.data.media.entity + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class SelectedMediaModel( + @SerializedName("mediaId") + var mediaId: Long, + @SerializedName("mediaName") + var mediaName: String, + @SerializedName("mediaPath") + var mediaPath: String, + @SerializedName("mediaSize") + var mediaSize: Long, + @SerializedName("mediaDuration") + var mediaDuration: Long, + @SerializedName("mediaAlbumCoverPath") + var mediaAlbumCoverPath: String, +) : Parcelable diff --git a/lassi/src/main/java/com/lassi/data/media/MediaRepositoryImpl.kt b/lassi/src/main/java/com/lassi/data/media/repository/MediaRepositoryImpl.kt similarity index 52% rename from lassi/src/main/java/com/lassi/data/media/MediaRepositoryImpl.kt rename to lassi/src/main/java/com/lassi/data/media/repository/MediaRepositoryImpl.kt index 11f8e8f..54d8f54 100644 --- a/lassi/src/main/java/com/lassi/data/media/MediaRepositoryImpl.kt +++ b/lassi/src/main/java/com/lassi/data/media/repository/MediaRepositoryImpl.kt @@ -1,4 +1,4 @@ -package com.lassi.data.media +package com.lassi.data.media.repository import android.annotation.SuppressLint import android.content.Context @@ -11,114 +11,146 @@ import com.lassi.common.extenstions.catch import com.lassi.common.utils.KeyUtils import com.lassi.common.utils.Logger import com.lassi.data.common.Result -import com.lassi.data.mediadirectory.Folder +import com.lassi.data.database.MediaFileDatabase +import com.lassi.data.media.MiItemMedia +import com.lassi.data.media.MiMedia +import com.lassi.data.media.entity.AlbumCoverPathEntity +import com.lassi.data.media.entity.DurationEntity +import com.lassi.data.media.entity.MediaFileEntity import com.lassi.domain.media.LassiConfig import com.lassi.domain.media.MediaRepository import com.lassi.domain.media.MediaType -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch import java.io.File -import java.util.* -import kotlin.collections.ArrayList class MediaRepositoryImpl(private val context: Context) : MediaRepository { - + val TAG = MediaRepositoryImpl::class.java.simpleName private val minTimeInMillis = LassiConfig.getConfig().minTime * 1000L private val maxTimeInMillis = LassiConfig.getConfig().maxTime * 1000L - private val fetchedFolders = arrayListOf() - private val folderMap = LinkedHashMap() private val minFileSize = LassiConfig.getConfig().minFileSize * 1024L private val maxFileSize = LassiConfig.getConfig().maxFileSize * 1024L + private lateinit var mediaDatabase: MediaFileDatabase - override suspend fun fetchFolders(): Flow>> { - return flow { - val projection = getProjections() - val cursor = query(projection) - cursor?.let { - folderMap.clear() - fetchedFolders.clear() - try { - if (cursor.moveToLast()) { - do { - val id = cursor.getLong(cursor.getColumnIndex(projection[0])) - val name = cursor.getString(cursor.getColumnIndex(projection[1])) - val path = cursor.getString(cursor.getColumnIndex(projection[2])) - val bucket = cursor.getString(cursor.getColumnIndex(projection[3])) - val size = - cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns.SIZE)) - val albumCoverPath = - if (LassiConfig.getConfig().mediaType == MediaType.AUDIO) { - val albumId = - cursor.getString(cursor.getColumnIndex(projection[5])) - if (albumId != null) { - getAlbumArt(albumId) - } else { - continue - } - } else { - "" - } - val duration = - if (LassiConfig.getConfig().mediaType == MediaType.VIDEO) { - cursor.getLong(cursor.getColumnIndex(projection[4])) - } else { - 0 - } + private fun initMediaDb(context: Context) { + if (!this::mediaDatabase.isInitialized) { + mediaDatabase = MediaFileDatabase.invoke(context = context) + Logger.d(TAG, "MEDIA FILE DATABASE Initialized ") + } + } - val file = makeSafeFile(path) - if (file != null && file.exists()) { - if (LassiConfig.getConfig().mediaType == MediaType.VIDEO || - LassiConfig.getConfig().mediaType == MediaType.AUDIO - ) { - checkDurationAndAddFileToFolder( - bucket, - id, - name, - path, - duration, - albumCoverPath, - size - ) - } else { - Logger.e("MediaRepositoryImpl", "$name >> $size") + override suspend fun isDbEmpty(): Boolean { + return try { + initMediaDb(context) + mediaDatabase.mediaFileDao().getDataCount( + mediaType = when (LassiConfig.getConfig().mediaType) { + MediaType.IMAGE -> MediaType.IMAGE.value + MediaType.VIDEO -> MediaType.VIDEO.value + MediaType.AUDIO -> MediaType.AUDIO.value + else -> MediaType.IMAGE.value + } + ) + } catch (e: Throwable) { + e.printStackTrace() + false + } + } - if (isValidFileSize(size)) { - addFileToFolder( - bucket, - MiMedia(id, name, path, duration, albumCoverPath) - ) - } - } - } - } while (cursor.moveToPrevious()) - } - } catch (e: Exception) { - Logger.e("MediaRepositoryImpl", "fetchFolders >> $e") - emit(Result.Error(Throwable())) - } finally { - cursor.close() + override suspend fun insertMediaData(): Result { + val resultDeferred = CoroutineScope(IO).async { + try { + initMediaDb(context) + val mediaType = when (LassiConfig.getConfig().mediaType) { + MediaType.IMAGE -> MediaType.IMAGE.value + MediaType.VIDEO -> MediaType.VIDEO.value + MediaType.AUDIO -> MediaType.AUDIO.value + else -> MediaType.IMAGE.value } - fetchedFolders.addAll(folderMap.values) - emit(Result.Success(fetchedFolders)) - } ?: emit(Result.Error(Throwable())) - }.catch().flowOn(Dispatchers.IO) + val latestImgFileDate: Long = mediaDatabase.mediaFileDao() + .getMaxDateFromMediaTable(mediaType) + return@async fetchAndInsertMediaHelper(latestImgFileDate) + } catch (e: Exception) { + return@async Result.Error(e) + } + } + return resultDeferred.await() + } + + override suspend fun insertAllMediaData(): Result { + val resultDeferred = CoroutineScope(IO).async { + try { + return@async fetchAndInsertMediaHelper(0L) //Here we don't need to pass Latest date + } catch (e: Exception) { + e.printStackTrace() + return@async Result.Error(e) + } + } + return resultDeferred.await() + } + + /** + * Prepare folder listing based on type + */ + override suspend fun getDataFromDb(): Flow>> { + return flow { + initMediaDb(context) + try { + val mediaType = when (LassiConfig.getConfig().mediaType) { + MediaType.IMAGE -> MediaType.IMAGE.value + MediaType.VIDEO -> MediaType.VIDEO.value + MediaType.AUDIO -> MediaType.AUDIO.value + else -> MediaType.IMAGE.value + } + val miItemMediaList = ArrayList() + mediaDatabase.mediaFileDao() + .getDistinctBucketList(mediaType) + .collect { folderList -> + miItemMediaList.clear() + folderList.forEach { bucket -> + val latestItemPathForBucket: String = mediaDatabase.mediaFileDao() + .getLatestItemForBucket(bucket = bucket, mediaType = mediaType) + val totalItemSizeForBucket: Long = mediaDatabase.mediaFileDao() + .getTotalItemSizeForBucket(bucket = bucket, mediaType = mediaType) + miItemMediaList.add( + MiItemMedia( + bucket, + latestItemPathForBucket, + totalItemSizeForBucket + ) + ) + } + emit(Result.Success(miItemMediaList)) + } + } catch (e: java.lang.Exception) { + e.printStackTrace() + emit(Result.Error(Throwable())) + } + }.catch().flowOn(IO) } - private fun checkDurationAndAddFileToFolder( + private suspend fun checkDurationAndAddFileToDatabase( bucket: String?, id: Long, name: String, path: String, duration: Long, albumCoverPath: String, - size: Long + size: Long, + dateAdded: Long, + mediaType: MediaType ) { if (isValidDuration(duration) && isValidFileSize(size)) { - addFileToFolder( + addFileToDatabase( bucket, - MiMedia(id, name, path, duration, albumCoverPath, size) + MiMedia(id, name, path, duration, albumCoverPath, size), + dateAdded, + mediaType ) } } @@ -166,20 +198,66 @@ class MediaRepositoryImpl(private val context: Context) : MediaRepository { } /** - * Add file to folder + * Add file to database */ - private fun addFileToFolder( + private suspend fun addFileToDatabase( bucket: String?, - miMedia: MiMedia + miMedia: MiMedia, + dateAdded: Long, + mediaType: MediaType, ) { val bucketName = bucket ?: context.getString(R.string.lassi_all) if (isFileTypeSupported(miMedia.path)) { - var folder = folderMap[bucketName] - if (folder == null) { - folder = Folder(bucketName) - folderMap[bucketName] = folder + CoroutineScope(IO).launch { + miMedia.path?.let { path -> + miMedia.name?.let { mName -> + MediaFileEntity( + miMedia.id, + mName, + path, + bucketName, + miMedia.fileSize, + dateAdded, + mediaType.value + ) + } + }?.let { mediaFileEntity -> + mediaDatabase.mediaFileDao().insertMediaFile(mediaFileEntity) //Insert Query + } + + when (LassiConfig.getConfig().mediaType) { + MediaType.AUDIO -> { + //to store duration -> DurationEntity + mediaDatabase.mediaFileDao() + .insertDuration(DurationEntity(miMedia.id, miMedia.duration)) + //to store albumCover -> AlbumCoverEntity + miMedia.thumb?.let { albumArtPath -> + AlbumCoverPathEntity(miMedia.id, albumArtPath) + }?.let { albumCoverPathEntity -> + mediaDatabase.mediaFileDao().insertAlbumCover(albumCoverPathEntity) + } + } + + MediaType.VIDEO -> { + //to store duration -> DurationEntity + mediaDatabase.mediaFileDao() + .insertDuration(DurationEntity(miMedia.id, miMedia.duration)) + } + else -> { + } + } + //to store duration -> DurationEntity + if (miMedia.duration != 0L) { + mediaDatabase.mediaFileDao() + .insertDuration(DurationEntity(miMedia.id, miMedia.duration)) + } + //to store albumCover -> AlbumCoverEntity + miMedia.thumb?.let { albumArtPath -> + AlbumCoverPathEntity(miMedia.id, albumArtPath) + }?.let { albumCoverPathEntity -> + mediaDatabase.mediaFileDao().insertAlbumCover(albumCoverPathEntity) + } } - folder.medias.add(miMedia) } } @@ -222,8 +300,9 @@ class MediaRepositoryImpl(private val context: Context) : MediaRepository { MediaStore.Images.Media._ID, MediaStore.Images.Media.TITLE, MediaStore.Images.Media.DATA, - MediaStore.Images.Media.BUCKET_DISPLAY_NAME, - MediaStore.Images.Media.SIZE + MediaStore.Images.Media.BUCKET_DISPLAY_NAME,//e.g. image file name + MediaStore.Images.Media.SIZE, + MediaStore.Images.Media.DATE_ADDED ) MediaType.VIDEO -> arrayOf( MediaStore.Video.Media._ID, @@ -231,6 +310,7 @@ class MediaRepositoryImpl(private val context: Context) : MediaRepository { MediaStore.Video.Media.DATA, MediaStore.Video.Media.BUCKET_DISPLAY_NAME, MediaStore.Video.VideoColumns.DURATION, + MediaStore.Video.Media.DATE_ADDED, MediaStore.Video.VideoColumns.SIZE ) MediaType.AUDIO -> arrayOf( @@ -239,6 +319,7 @@ class MediaRepositoryImpl(private val context: Context) : MediaRepository { MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.AudioColumns.DURATION, + MediaStore.Audio.Media.DATE_ADDED, MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.SIZE ) @@ -252,19 +333,21 @@ class MediaRepositoryImpl(private val context: Context) : MediaRepository { } } - private fun query(projection: Array): Cursor? { + private fun query(projection: Array, latestImageFileDate: Long): Cursor? { return when (LassiConfig.getConfig().mediaType) { - MediaType.IMAGE -> context.contentResolver.query( - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) - } else { - MediaStore.Images.Media.EXTERNAL_CONTENT_URI - }, - projection, - null, - null, - MediaStore.Images.Media.DATE_ADDED - ) + MediaType.IMAGE -> { + context.contentResolver.query( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) + } else { + MediaStore.Images.Media.EXTERNAL_CONTENT_URI + }, + projection, + if (latestImageFileDate != 0L) "${MediaStore.Images.Media.DATE_ADDED} > ?" else null, + if (latestImageFileDate != 0L) arrayOf(latestImageFileDate.toString()) else null, + MediaStore.Images.Media.DATE_ADDED + ) + } MediaType.VIDEO -> context.contentResolver.query( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) @@ -272,8 +355,8 @@ class MediaRepositoryImpl(private val context: Context) : MediaRepository { MediaStore.Video.Media.EXTERNAL_CONTENT_URI }, projection, - null, - null, + if (latestImageFileDate != 0L) "${MediaStore.Video.Media.DATE_ADDED} > ?" else null, + if (latestImageFileDate != 0L) arrayOf(latestImageFileDate.toString()) else null, MediaStore.Video.Media.DATE_ADDED ) MediaType.AUDIO -> context.contentResolver.query( @@ -285,8 +368,8 @@ class MediaRepositoryImpl(private val context: Context) : MediaRepository { MediaStore.Audio.Media.EXTERNAL_CONTENT_URI }, projection, - null, - null, + if (latestImageFileDate != 0L) "${MediaStore.Audio.Media.DATE_ADDED} > ?" else null, + if (latestImageFileDate != 0L) arrayOf(latestImageFileDate.toString()) else null, MediaStore.Audio.Media.DATE_ADDED ) MediaType.DOC -> { @@ -308,12 +391,99 @@ class MediaRepositoryImpl(private val context: Context) : MediaRepository { MediaStore.Video.Media.DATE_ADDED ) } - else ->{ + else -> { null } } } + @SuppressLint("Range") + private suspend fun fetchAndInsertMediaHelper(latestImgFileDate: Long): Result { + val result: Deferred?> = CoroutineScope(IO).async { + val projection = getProjections() + val cursor: Cursor? = query(projection, latestImgFileDate) + cursor?.let { + try { + if (cursor.moveToLast()) { + do { + val id = cursor.getLong(cursor.getColumnIndex(projection[0])) + val name = cursor.getString(cursor.getColumnIndex(projection[1])) + val path = cursor.getString(cursor.getColumnIndex(projection[2])) + val bucket = cursor.getString(cursor.getColumnIndex(projection[3])) + val size = + cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns.SIZE)) + val albumCoverPath = + if (LassiConfig.getConfig().mediaType == MediaType.AUDIO) { + val albumId = + cursor.getString(cursor.getColumnIndex(projection[6])) + if (albumId != null) { + getAlbumArt(albumId) + } else { + continue + } + } else { + "" + } + val duration = + if (LassiConfig.getConfig().mediaType == MediaType.VIDEO || LassiConfig.getConfig().mediaType == MediaType.AUDIO) { + cursor.getLong(cursor.getColumnIndex(projection[4])) + } else { + 0 + } + + val dateAdded = + cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATE_ADDED)) + + val file = makeSafeFile(path) + + initMediaDb(context) + + if (file != null && file.exists()) { + if (LassiConfig.getConfig().mediaType == MediaType.VIDEO || + LassiConfig.getConfig().mediaType == MediaType.AUDIO + ) { + checkDurationAndAddFileToDatabase( + bucket, + id, + name, + path, + duration, + albumCoverPath, + size, + dateAdded, + LassiConfig.getConfig().mediaType + ) + } else { + if (isValidFileSize(size)) { + addFileToDatabase( + bucket, + MiMedia( + id, + name, + path, + duration, + thumb = albumCoverPath + ), + dateAdded, + LassiConfig.getConfig().mediaType + ) + } + } + } + } while (cursor.moveToPrevious()) + } + return@async Result.Success(true) + } catch (e: Exception) { + e.printStackTrace() + return@async Result.Error(e) + } finally { + cursor.close() + } + } + } + return result.await()!! + } + /** * fetch album art for audio files */ @@ -345,9 +515,8 @@ class MediaRepositoryImpl(private val context: Context) : MediaRepository { override suspend fun fetchDocs(): Flow>> { return flow { val projection = getProjections() - val cursor = query(projection) + val cursor = query(projection, 0L) cursor?.let { - Logger.e("MediaRepositoryImpl", "Fetch documents size ${cursor.count}") val docs = ArrayList() try { if (cursor.moveToLast()) { @@ -359,12 +528,12 @@ class MediaRepositoryImpl(private val context: Context) : MediaRepository { } while (cursor.moveToPrevious()) } } catch (e: Exception) { - Logger.e("MediaRepositoryImpl", "fetchFolders >> $e") + e.printStackTrace() } finally { cursor.close() } emit(Result.Success(docs)) } ?: emit(Result.Error(Throwable())) - }.catch().flowOn(Dispatchers.IO) + }.catch().flowOn(IO) } } diff --git a/lassi/src/main/java/com/lassi/data/media/repository/SelectedMediaRepositoryImpl.kt b/lassi/src/main/java/com/lassi/data/media/repository/SelectedMediaRepositoryImpl.kt new file mode 100644 index 0000000..551b2b6 --- /dev/null +++ b/lassi/src/main/java/com/lassi/data/media/repository/SelectedMediaRepositoryImpl.kt @@ -0,0 +1,82 @@ +package com.lassi.data.media.repository + +import android.content.Context +import com.lassi.common.extenstions.catch +import com.lassi.common.utils.Logger +import com.lassi.data.common.Result +import com.lassi.data.database.MediaFileDatabase +import com.lassi.data.media.MiMedia +import com.lassi.data.media.entity.MediaFileEntity +import com.lassi.data.media.entity.SelectedMediaModel +import com.lassi.domain.media.LassiConfig +import com.lassi.domain.media.MediaType +import com.lassi.domain.media.SelectedMediaRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +class SelectedMediaRepositoryImpl(private val context: Context) : SelectedMediaRepository { + val TAG = SelectedMediaRepositoryImpl::class.java.simpleName + private lateinit var mediaDatabase: MediaFileDatabase + private val miMediaFileEntityList = ArrayList() + + override suspend fun getSelectedMediaData(bucket: String): Flow>> { + return flow { + initMediaDb(context) + try { + miMediaFileEntityList.clear() + + val mediaType = when (LassiConfig.getConfig().mediaType) { + MediaType.IMAGE -> MediaType.IMAGE.value + MediaType.VIDEO -> MediaType.VIDEO.value + MediaType.AUDIO -> MediaType.AUDIO.value + else -> MediaType.IMAGE.value + } + + val selectedImageMediaItemList: List + val selectedMediaItemList: List + if (mediaType == MediaType.IMAGE.value) { + selectedImageMediaItemList = + mediaDatabase.mediaFileDao().getSelectedImageMediaFile(bucket, mediaType) + selectedImageMediaItemList.forEach { selectedMediaModel -> + miMediaFileEntityList.add( + MiMedia( + id = selectedMediaModel.mediaId, + name = selectedMediaModel.mediaName, + path = selectedMediaModel.mediaPath, + fileSize = selectedMediaModel.mediaSize, + ) + ) + } + } else { + selectedMediaItemList = + mediaDatabase.mediaFileDao().getSelectedMediaFile(bucket, mediaType) + selectedMediaItemList.forEach { selectedMediaModel -> + miMediaFileEntityList.add( + MiMedia( + id = selectedMediaModel.mediaId, + name = selectedMediaModel.mediaName, + path = selectedMediaModel.mediaPath, + fileSize = selectedMediaModel.mediaSize, + duration = selectedMediaModel.mediaDuration, + thumb = selectedMediaModel.mediaAlbumCoverPath, + ) + ) + } + } + emit(Result.Success(miMediaFileEntityList)) + } catch (e: java.lang.Exception) { + e.printStackTrace() + emit(Result.Error(Throwable())) + } + }.catch().flowOn(Dispatchers.IO) + } + + private fun initMediaDb(context: Context) { + if (!this::mediaDatabase.isInitialized) { + mediaDatabase = MediaFileDatabase.invoke(context = context) + Logger.d(TAG, "MEDIA FILE DATABASE Initialized ") + } + } +} \ No newline at end of file diff --git a/lassi/src/main/java/com/lassi/data/mediadirectory/FolderBucket.kt b/lassi/src/main/java/com/lassi/data/mediadirectory/FolderBucket.kt new file mode 100644 index 0000000..326cf15 --- /dev/null +++ b/lassi/src/main/java/com/lassi/data/mediadirectory/FolderBucket.kt @@ -0,0 +1,12 @@ +package com.lassi.data.mediadirectory + +import android.os.Parcelable +import com.lassi.data.media.MiMedia +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class FolderBucket( + var bucketName: String?, + var totalItems: ArrayList = ArrayList(), + var lastImagePath: String? +) : Parcelable diff --git a/lassi/src/main/java/com/lassi/domain/media/MediaRepository.kt b/lassi/src/main/java/com/lassi/domain/media/MediaRepository.kt index b64e840..531c1b4 100644 --- a/lassi/src/main/java/com/lassi/domain/media/MediaRepository.kt +++ b/lassi/src/main/java/com/lassi/domain/media/MediaRepository.kt @@ -1,11 +1,14 @@ package com.lassi.domain.media import com.lassi.data.common.Result +import com.lassi.data.media.MiItemMedia import com.lassi.data.media.MiMedia -import com.lassi.data.mediadirectory.Folder import kotlinx.coroutines.flow.Flow interface MediaRepository { - suspend fun fetchFolders(): Flow>> + suspend fun getDataFromDb(): Flow>> suspend fun fetchDocs(): Flow>> + suspend fun isDbEmpty(): Boolean + suspend fun insertMediaData(): Result + suspend fun insertAllMediaData(): Result } \ No newline at end of file diff --git a/lassi/src/main/java/com/lassi/domain/media/SelectedMediaRepository.kt b/lassi/src/main/java/com/lassi/domain/media/SelectedMediaRepository.kt new file mode 100644 index 0000000..28b9dba --- /dev/null +++ b/lassi/src/main/java/com/lassi/domain/media/SelectedMediaRepository.kt @@ -0,0 +1,9 @@ +package com.lassi.domain.media + +import com.lassi.data.common.Result +import com.lassi.data.media.MiMedia +import kotlinx.coroutines.flow.Flow + +interface SelectedMediaRepository { + suspend fun getSelectedMediaData(bucket: String): Flow>> +} \ No newline at end of file diff --git a/lassi/src/main/java/com/lassi/presentation/docs/DocsViewModelFactory.kt b/lassi/src/main/java/com/lassi/presentation/docs/DocsViewModelFactory.kt index 684f67c..e077d28 100644 --- a/lassi/src/main/java/com/lassi/presentation/docs/DocsViewModelFactory.kt +++ b/lassi/src/main/java/com/lassi/presentation/docs/DocsViewModelFactory.kt @@ -3,7 +3,7 @@ package com.lassi.presentation.docs import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.lassi.data.media.MediaRepositoryImpl +import com.lassi.data.media.repository.MediaRepositoryImpl import com.lassi.domain.media.MediaRepository @Suppress("UNCHECKED_CAST") diff --git a/lassi/src/main/java/com/lassi/presentation/media/MediaFragment.kt b/lassi/src/main/java/com/lassi/presentation/media/MediaFragment.kt index 8258705..9df254d 100644 --- a/lassi/src/main/java/com/lassi/presentation/media/MediaFragment.kt +++ b/lassi/src/main/java/com/lassi/presentation/media/MediaFragment.kt @@ -13,29 +13,33 @@ import com.lassi.R import com.lassi.common.utils.CropUtils import com.lassi.common.utils.KeyUtils import com.lassi.common.utils.KeyUtils.SELECTED_FOLDER +import com.lassi.common.utils.Logger +import com.lassi.data.common.Response +import com.lassi.data.media.MiItemMedia import com.lassi.data.media.MiMedia -import com.lassi.data.mediadirectory.Folder +import com.lassi.domain.common.SafeObserver import com.lassi.domain.media.LassiConfig import com.lassi.domain.media.MediaType import com.lassi.presentation.common.LassiBaseViewModelFragment import com.lassi.presentation.common.decoration.GridSpacingItemDecoration import com.lassi.presentation.media.adapter.MediaAdapter +import com.lassi.presentation.mediadirectory.SelectedMediaViewModelFactory import com.lassi.presentation.videopreview.VideoPreviewActivity import kotlinx.android.synthetic.main.fragment_media_picker.* import java.io.File class MediaFragment : LassiBaseViewModelFragment() { private val mediaAdapter by lazy { MediaAdapter(this::onItemClick) } - private var folder: Folder? = null + private var bucket: MiItemMedia? = null private var mediaPickerConfig = LassiConfig.getConfig() override fun getContentResource() = R.layout.fragment_media_picker companion object { - fun getInstance(folder: Folder): MediaFragment { + fun getInstance(bucket: MiItemMedia): MediaFragment { val miMediaPickerFragment = MediaFragment() val args = Bundle().apply { - putParcelable(SELECTED_FOLDER, folder) + putParcelable(SELECTED_FOLDER, bucket) } miMediaPickerFragment.arguments = args return miMediaPickerFragment @@ -44,7 +48,11 @@ class MediaFragment : LassiBaseViewModelFragment() { override fun initViews() { super.initViews() - setImageAdapter() + bucket?.let { + it.bucketName?.let { bucketName -> + viewModel.getSelectedMediaData(bucket = bucketName) + } + } progressBar.indeterminateDrawable.colorFilter = BlendModeColorFilterCompat.createBlendModeColorFilterCompat( mediaPickerConfig.progressBarColor, @@ -55,19 +63,41 @@ class MediaFragment : LassiBaseViewModelFragment() { override fun getBundle() { super.getBundle() arguments?.let { - folder = it.getParcelable(SELECTED_FOLDER) + bucket = it.getParcelable(SELECTED_FOLDER) } } override fun buildViewModel(): SelectedMediaViewModel { - return ViewModelProvider(requireActivity())[SelectedMediaViewModel::class.java] + return ViewModelProvider( + requireActivity(), + SelectedMediaViewModelFactory(requireActivity()) + )[SelectedMediaViewModel::class.java] + } + + override fun initLiveDataObservers() { + super.initLiveDataObservers() + + viewModel.fetchedMediaLiveData.observe( + viewLifecycleOwner, + SafeObserver(::handleFetchedData) + ) } - private fun setImageAdapter() { + private fun handleFetchedData(response: Response>?) { rvMedia.layoutManager = GridLayoutManager(context, mediaPickerConfig.gridSize) rvMedia.adapter = mediaAdapter rvMedia.addItemDecoration(GridSpacingItemDecoration(mediaPickerConfig.gridSize, 10)) - mediaAdapter.setList(folder?.medias) + + when (response) { + is Response.Success -> { + Logger.d("mediaFragment", "handleFetchedData SUCCESS size -> ${response.item.size}") + mediaAdapter.setList(response.item) + } + is Response.Error -> { + Logger.d("mediaFragment", "handleFetchedData ERROR") + } + else -> {} + } } private fun onItemClick(selectedMedias: ArrayList) { @@ -94,6 +124,7 @@ class MediaFragment : LassiBaseViewModelFragment() { ) } } + else -> {} } } diff --git a/lassi/src/main/java/com/lassi/presentation/media/SelectedMediaViewModel.kt b/lassi/src/main/java/com/lassi/presentation/media/SelectedMediaViewModel.kt index b75451a..645ce1f 100644 --- a/lassi/src/main/java/com/lassi/presentation/media/SelectedMediaViewModel.kt +++ b/lassi/src/main/java/com/lassi/presentation/media/SelectedMediaViewModel.kt @@ -1,14 +1,23 @@ package com.lassi.presentation.media import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.lassi.data.common.Response +import com.lassi.data.common.Result import com.lassi.data.media.MiMedia +import com.lassi.domain.media.SelectedMediaRepository import com.lassi.presentation.common.LassiBaseViewModel -import java.util.* +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch -class SelectedMediaViewModel : LassiBaseViewModel() { +class SelectedMediaViewModel( + private val selectedMediaRepository: SelectedMediaRepository +) : LassiBaseViewModel() { val selectedMediaLiveData = MutableLiveData>() private var selectedMedias = arrayListOf() + var fetchedMediaLiveData = MutableLiveData>>() + fun addAllSelectedMedia(selectedMedias: ArrayList) { this.selectedMedias = selectedMedias this.selectedMedias = this.selectedMedias.distinctBy { @@ -19,9 +28,21 @@ class SelectedMediaViewModel : LassiBaseViewModel() { fun addSelectedMedia(selectedMedia: MiMedia) { this.selectedMedias.add(selectedMedia) - this.selectedMedias = this.selectedMedias.distinctBy { - it.path - } as ArrayList + this.selectedMedias = this.selectedMedias.distinctBy { it.path } as ArrayList selectedMediaLiveData.value = this.selectedMedias } + + fun getSelectedMediaData(bucket: String) { + viewModelScope.launch { + selectedMediaRepository.getSelectedMediaData(bucket).onStart { + fetchedMediaLiveData.value = Response.Loading() + }.collect { result -> + when (result) { + is Result.Success -> result.data.let { fetchedMediaLiveData.postValue(Response.Success(it)) } + is Result.Error -> fetchedMediaLiveData.value = (Response.Error(result.throwable)) + else -> {} + } + } + } + } } \ No newline at end of file diff --git a/lassi/src/main/java/com/lassi/presentation/media/adapter/MediaAdapter.kt b/lassi/src/main/java/com/lassi/presentation/media/adapter/MediaAdapter.kt index fa2f8b1..043530e 100644 --- a/lassi/src/main/java/com/lassi/presentation/media/adapter/MediaAdapter.kt +++ b/lassi/src/main/java/com/lassi/presentation/media/adapter/MediaAdapter.kt @@ -14,7 +14,6 @@ import com.lassi.data.media.MiMedia import com.lassi.domain.media.LassiConfig import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.item_media.* -import java.util.* class MediaAdapter( private val onItemClick: (selectedMedias: ArrayList) -> Unit diff --git a/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderFragment.kt b/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderFragment.kt index 4965976..be972cc 100644 --- a/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderFragment.kt +++ b/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderFragment.kt @@ -8,6 +8,7 @@ import android.os.Build import android.os.Bundle import android.provider.Settings import android.view.Menu +import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.graphics.BlendModeColorFilterCompat @@ -16,10 +17,11 @@ import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import com.lassi.R import com.lassi.common.extenstions.hide +import com.lassi.common.extenstions.safeObserve import com.lassi.common.extenstions.show +import com.lassi.common.utils.Logger import com.lassi.data.common.Response -import com.lassi.data.mediadirectory.Folder -import com.lassi.domain.common.SafeObserver +import com.lassi.data.media.MiItemMedia import com.lassi.domain.media.LassiConfig import com.lassi.domain.media.LassiOption import com.lassi.domain.media.MediaType @@ -30,6 +32,7 @@ import com.lassi.presentation.mediadirectory.adapter.FolderAdapter import kotlinx.android.synthetic.main.fragment_media_picker.* class FolderFragment : LassiBaseViewModelFragment() { + companion object { fun newInstance(): FolderFragment { return FolderFragment() @@ -44,7 +47,7 @@ class FolderFragment : LassiBaseViewModelFragment() { private val requestPermission = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { map -> if (map.entries.all { it.value }) { - fetchFolders() + viewModel.checkInsert() } else { showPermissionDisableAlert() } @@ -75,18 +78,40 @@ class FolderFragment : LassiBaseViewModelFragment() { override fun initLiveDataObservers() { super.initLiveDataObservers() - viewModel.fetchMediaFolderLiveData.observe( - viewLifecycleOwner, - SafeObserver(this::handleFetchedFolders) - ) + viewModel.fetchMediaFolderLiveData.safeObserve(viewLifecycleOwner) { response -> + when (response) { + is Response.Loading -> { + tvNoDataFound.visibility = View.GONE + progressBar.show() + } + is Response.Success -> {} + is Response.Error -> { + progressBar.hide() + response.throwable.printStackTrace() + } + } + } + + viewModel.getMediaItemList().observe(viewLifecycleOwner) { + progressBar.hide() + if (!it.isNullOrEmpty()) { + folderAdapter.setList(it) + } + } + + viewModel.emptyList.observe(viewLifecycleOwner) { + if (it) { + tvNoDataFound.visibility = View.VISIBLE + } else { + tvNoDataFound.visibility = View.GONE + } + } } private fun requestPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { requestPermission.launch( - arrayOf( - Manifest.permission.READ_EXTERNAL_STORAGE - ) + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) ) } else { requestPermission.launch( @@ -98,22 +123,7 @@ class FolderFragment : LassiBaseViewModelFragment() { } } - private fun fetchFolders() { - viewModel.fetchFolders() - } - - private fun handleFetchedFolders(response: Response>) { - when (response) { - is Response.Success -> { - progressBar.hide() - folderAdapter.setList(response.item) - } - is Response.Loading -> progressBar.show() - is Response.Error -> progressBar.hide() - } - } - - private fun onItemClick(folder: Folder) { + private fun onItemClick(bucket: MiItemMedia) { activity?.supportFragmentManager?.beginTransaction() ?.setCustomAnimations( R.anim.right_in, @@ -121,7 +131,7 @@ class FolderFragment : LassiBaseViewModelFragment() { R.anim.right_in, R.anim.right_out ) - ?.add(R.id.ftContainer, MediaFragment.getInstance(folder)) + ?.add(R.id.ftContainer, MediaFragment.getInstance(bucket)) ?.addToBackStack(MediaFragment::class.java.simpleName) ?.commitAllowingStateLoss() } diff --git a/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderViewModel.kt b/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderViewModel.kt index b3e307d..81cf82f 100644 --- a/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderViewModel.kt +++ b/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderViewModel.kt @@ -1,41 +1,101 @@ package com.lassi.presentation.mediadirectory +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.lassi.common.extenstions.setError +import com.lassi.common.extenstions.setLoading +import com.lassi.common.utils.Logger import com.lassi.data.common.Response import com.lassi.data.common.Result -import com.lassi.data.mediadirectory.Folder +import com.lassi.data.media.MiItemMedia +import com.lassi.domain.common.SingleLiveEvent import com.lassi.domain.media.MediaRepository import com.lassi.presentation.common.LassiBaseViewModel -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class FolderViewModel( private val mediaRepository: MediaRepository ) : LassiBaseViewModel() { - var fetchMediaFolderLiveData = MutableLiveData>>() + private var _fetchMediaFolderLiveData = SingleLiveEvent>>() + var fetchMediaFolderLiveData: LiveData>> = + _fetchMediaFolderLiveData + val list: MutableLiveData> = MutableLiveData() + var emptyList: MutableLiveData = MutableLiveData(false) - fun fetchFolders() { + fun checkInsert() { viewModelScope.launch { - mediaRepository.fetchFolders() - .onStart { - fetchMediaFolderLiveData.postValue(Response.Loading()) - }.collect { result -> - when (result) { - is Result.Success -> { - fetchMediaFolderLiveData.postValue(Response.Success(result.data)) - } - is Result.Error -> { - fetchMediaFolderLiveData.postValue(Response.Error(result.throwable)) - } - else -> { - /** - * no need to implement - */ - } - } + withContext(Dispatchers.IO) { + this@FolderViewModel._fetchMediaFolderLiveData.setLoading() + if (mediaRepository.isDbEmpty()) { + insertDataInDatabase() + } else { + checkAndInsertNewDataIntoDatabase() } + } + } + } + + fun getMediaItemList(): LiveData> { + return list + } + + private suspend fun checkAndInsertNewDataIntoDatabase() { + when (val result = mediaRepository.insertMediaData()) { + is Result.Loading -> { + this._fetchMediaFolderLiveData.setLoading() + } + is Result.Success -> { + getDataFromDatabase() + } + is Result.Error -> { + this._fetchMediaFolderLiveData.setError(result.throwable) + } + } + } + + private suspend fun insertDataInDatabase() { + when (val result = mediaRepository.insertAllMediaData()) { + is Result.Loading -> { + this._fetchMediaFolderLiveData.setLoading() + } + is Result.Success -> { + Logger.d("FolderViewModel", "Insert completed") + getDataFromDatabase() + } + is Result.Error -> { + this._fetchMediaFolderLiveData.setError(result.throwable) + } } } + + private suspend fun getDataFromDatabase() { + mediaRepository.getDataFromDb() + .onStart { + _fetchMediaFolderLiveData.setLoading() + } + .map { result -> + when (result) { + is Result.Success -> result.data.filter { + !it.bucketName.isNullOrEmpty() + } + is Result.Error -> TODO() + Result.Loading -> TODO() + } + } + .collectLatest { mediaItemList -> + withContext(Dispatchers.Main) { + list.value = mediaItemList as ArrayList + + if (mediaItemList.isNullOrEmpty()) { + emptyList.value = true + } + } + } + } } \ No newline at end of file diff --git a/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderViewModelFactory.kt b/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderViewModelFactory.kt index bdd652d..6aa8f97 100644 --- a/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderViewModelFactory.kt +++ b/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderViewModelFactory.kt @@ -3,7 +3,7 @@ package com.lassi.presentation.mediadirectory import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.lassi.data.media.MediaRepositoryImpl +import com.lassi.data.media.repository.MediaRepositoryImpl import com.lassi.domain.media.MediaRepository @Suppress("UNCHECKED_CAST") diff --git a/lassi/src/main/java/com/lassi/presentation/mediadirectory/LassiMediaPickerActivity.kt b/lassi/src/main/java/com/lassi/presentation/mediadirectory/LassiMediaPickerActivity.kt index 6a9e554..c86cc71 100644 --- a/lassi/src/main/java/com/lassi/presentation/mediadirectory/LassiMediaPickerActivity.kt +++ b/lassi/src/main/java/com/lassi/presentation/mediadirectory/LassiMediaPickerActivity.kt @@ -60,7 +60,10 @@ class LassiMediaPickerActivity : LassiBaseViewModelActivity LassiConfig.getConfig().selectedMedias.addAll(selectedMedia) viewModel.addAllSelectedMedia(selectedMedia) - folderViewModel.fetchFolders() + folderViewModel.checkInsert() if (LassiConfig.getConfig().lassiOption == LassiOption.CAMERA_AND_GALLERY) { supportFragmentManager.popBackStack() } @@ -267,7 +270,7 @@ class LassiMediaPickerActivity : LassiBaseViewModelActivity create(modelClass: Class): T { + return SelectedMediaViewModel(selectedMediaRepository) as T + } +} \ No newline at end of file diff --git a/lassi/src/main/java/com/lassi/presentation/mediadirectory/adapter/FolderAdapter.kt b/lassi/src/main/java/com/lassi/presentation/mediadirectory/adapter/FolderAdapter.kt index d1d3d12..f4a244d 100644 --- a/lassi/src/main/java/com/lassi/presentation/mediadirectory/adapter/FolderAdapter.kt +++ b/lassi/src/main/java/com/lassi/presentation/mediadirectory/adapter/FolderAdapter.kt @@ -8,21 +8,20 @@ import com.lassi.common.extenstions.hide import com.lassi.common.extenstions.inflate import com.lassi.common.extenstions.loadImage import com.lassi.common.extenstions.show -import com.lassi.common.utils.ImageUtils -import com.lassi.data.mediadirectory.Folder +import com.lassi.data.media.MiItemMedia import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.item_media.* class FolderAdapter( - private val onItemClick: (folder: Folder) -> Unit + private val onItemClick: (bucket: MiItemMedia) -> Unit ) : RecyclerView.Adapter() { - private var folders = ArrayList() + private var buckets = ArrayList() - fun setList(folders: ArrayList?) { - folders?.let { - this.folders.clear() - this.folders.addAll(it) + fun setList(buckets: ArrayList?) { + buckets?.let { + this.buckets.clear() + this.buckets.addAll(it) } notifyDataSetChanged() } @@ -31,26 +30,32 @@ class FolderAdapter( return FolderViewHolder(parent.inflate(R.layout.item_media)) } - override fun getItemCount() = folders.size + override fun getItemCount() = buckets.size override fun onBindViewHolder(holder: FolderViewHolder, position: Int) { - holder.bind(folders[position]) + holder.bind(buckets[position]) + } + + fun clear() { + val size: Int = buckets.size + buckets.clear() + notifyItemRangeRemoved(0, size) } inner class FolderViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), LayoutContainer { - fun bind(folder: Folder) { - with(folder) { + fun bind(bucket: MiItemMedia) { + with(bucket) { tvFolderName.show() tvDuration.hide() - ivFolderThumbnail.loadImage(ImageUtils.getThumb(medias[0])) + ivFolderThumbnail.loadImage(bucket.latestItemPathForBucket) tvFolderName.text = String.format( tvFolderName.context.getString(R.string.directory_with_item_count), - folderName, - medias.size.toString() + bucketName, + totalItemSizeForBucket.toString() ) itemView.setOnClickListener { - onItemClick(folder) + onItemClick(bucket) } } } diff --git a/lassi/src/main/java/com/lassi/presentation/videopreview/VideoPreviewActivity.kt b/lassi/src/main/java/com/lassi/presentation/videopreview/VideoPreviewActivity.kt index 909ea66..dba70d1 100644 --- a/lassi/src/main/java/com/lassi/presentation/videopreview/VideoPreviewActivity.kt +++ b/lassi/src/main/java/com/lassi/presentation/videopreview/VideoPreviewActivity.kt @@ -18,7 +18,6 @@ import androidx.fragment.app.FragmentActivity import com.lassi.R import com.lassi.common.utils.FilePickerUtils import com.lassi.common.utils.KeyUtils -import com.lassi.common.utils.Logger import com.lassi.data.media.MiMedia import com.lassi.domain.media.LassiConfig import com.lassi.presentation.common.LassiBaseActivity @@ -130,7 +129,7 @@ class VideoPreviewActivity : LassiBaseActivity() { finish() } catch (e: Exception) { - Logger.e(logTag, "onFileScanComplete $e") + e.printStackTrace() } finally { cursor.close() } diff --git a/lassi/src/main/res/layout/fragment_media_picker.xml b/lassi/src/main/res/layout/fragment_media_picker.xml index 68bc925..ada0c3b 100644 --- a/lassi/src/main/res/layout/fragment_media_picker.xml +++ b/lassi/src/main/res/layout/fragment_media_picker.xml @@ -18,6 +18,20 @@ tools:listitem="@layout/item_media" tools:spanCount="2" /> + + #80000000 #50000000 + #FFA000 diff --git a/lassi/src/main/res/values/strings.xml b/lassi/src/main/res/values/strings.xml index e11ecaf..7061319 100644 --- a/lassi/src/main/res/values/strings.xml +++ b/lassi/src/main/res/values/strings.xml @@ -22,4 +22,7 @@ Camera and/or Storage permissions are not granted. Please allow it from setting. Storage permission is not granted. Please allow it from setting. Record video longer then %s + + + No Data Found!