-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrated Custom Image Selector to Jetpack Compose #5964
Draft
rohit9625
wants to merge
24
commits into
commons-app:main
Choose a base branch
from
rohit9625:jetpack-custom-selector
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
b0bf55b
UI: add material theme for compose
rohit9625 3f3df7d
add ui components for custom selector
rohit9625 8558594
add adaptive layout, image loading, and compose navigation dependencies
rohit9625 74cf035
add logic to fetch images from storage and manage in viewmodel
rohit9625 9f7ae75
create custom selector main screen with UI events and folder data class
rohit9625 0ff2880
create image grid screen/pane to display images from any folder
rohit9625 5da9097
refactor: add new cs screen into custom selector activity
rohit9625 1a86883
add actions lambda to bottom bar and replace hard-coded strings
rohit9625 ca30bf1
Add selection count indicator in top bar and refactor
rohit9625 55c0939
Add drag and tap gestures to select images
rohit9625 31c012f
refactor holder screen for both folder and image panes
rohit9625 071bffb
refactor preview for folder item
rohit9625 a930d8e
add view image screen and enable edge to edge for custom selector
rohit9625 dcd31f9
update dependencies
rohit9625 130158f
remove state-changing argument causing unnecessary recompositions
rohit9625 03713dd
add functionality for unselecting all pictures at once
rohit9625 178154c
fix overlapping navigation bar adding navigation bar padding
rohit9625 4ebb945
Merge branch 'main' into jetpack-custom-selector
rohit9625 c688059
remove onBackPressed override
rohit9625 c033003
move models package inside domain package
rohit9625 1d11ab7
fix imports and refactor code
rohit9625 443e713
refactor Ui components for custom selector
rohit9625 e611cbc
update UI states and refactor code
rohit9625 d1fdab4
add logic to mark or unmark images as not for upload
rohit9625 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
app/src/main/java/fr/free/nrw/commons/customselector/data/ImageRepositoryImpl.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package fr.free.nrw.commons.customselector.data | ||
|
||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus | ||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao | ||
import fr.free.nrw.commons.customselector.domain.ImageRepository | ||
import fr.free.nrw.commons.customselector.domain.model.Image | ||
import kotlinx.coroutines.flow.Flow | ||
import javax.inject.Inject | ||
|
||
class ImageRepositoryImpl @Inject constructor( | ||
private val mediaReader: MediaReader, | ||
private val notForUploadStatusDao: NotForUploadStatusDao | ||
): ImageRepository { | ||
override suspend fun getImagesFromDevice(): Flow<Image> { | ||
return mediaReader.getImages() | ||
} | ||
|
||
override suspend fun markAsNotForUpload(imageSHA: String) { | ||
notForUploadStatusDao.insert(NotForUploadStatus(imageSHA)) | ||
} | ||
|
||
override suspend fun unmarkAsNotForUpload(imageSHA: String) { | ||
notForUploadStatusDao.deleteWithImageSHA1(imageSHA) | ||
} | ||
|
||
override suspend fun isNotForUpload(imageSHA: String): Boolean { | ||
return notForUploadStatusDao.find(imageSHA) > 0 | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
app/src/main/java/fr/free/nrw/commons/customselector/data/MediaReader.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package fr.free.nrw.commons.customselector.data | ||
|
||
import android.content.ContentUris | ||
import android.content.Context | ||
import android.provider.MediaStore | ||
import android.text.format.DateFormat | ||
import fr.free.nrw.commons.customselector.domain.model.Image | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.flow.flow | ||
import kotlinx.coroutines.flow.flowOn | ||
import java.util.Calendar | ||
import java.util.Date | ||
import javax.inject.Inject | ||
|
||
class MediaReader @Inject constructor(private val context: Context) { | ||
fun getImages() = flow { | ||
val projection = arrayOf( | ||
MediaStore.Images.Media._ID, | ||
MediaStore.Images.Media.DISPLAY_NAME, | ||
MediaStore.Images.Media.DATA, | ||
MediaStore.Images.Media.BUCKET_ID, | ||
MediaStore.Images.Media.BUCKET_DISPLAY_NAME, | ||
MediaStore.Images.Media.DATE_ADDED, | ||
MediaStore.Images.Media.MIME_TYPE | ||
) | ||
val cursor = context.contentResolver.query( | ||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, | ||
null, null, MediaStore.Images.Media.DATE_ADDED + " DESC" | ||
) | ||
|
||
cursor?.use { | ||
val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID) | ||
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) | ||
val dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA) | ||
val bucketIdColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID) | ||
val bucketNameColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME) | ||
val dateColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED) | ||
val mimeTypeColumn = cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE) | ||
|
||
while(cursor.moveToNext()) { | ||
val id = cursor.getLong(idColumn) | ||
val name = cursor.getString(nameColumn) | ||
val path = cursor.getString(dataColumn) | ||
val bucketId = cursor.getLong(bucketIdColumn) | ||
val bucketName = cursor.getString(bucketNameColumn) | ||
val date = cursor.getLong(dateColumn) | ||
val mimeType = cursor.getString(mimeTypeColumn) | ||
|
||
val validMimeTypes = arrayOf( | ||
"image/jpeg", "image/png", "image/svg+xml", "image/gif", | ||
"image/tiff", "image/webp", "image/x-xcf" | ||
) | ||
// Skip the media items with unsupported MIME types | ||
if(mimeType.lowercase() !in validMimeTypes) continue | ||
|
||
// URI to access the image | ||
val uri = ContentUris.withAppendedId( | ||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id | ||
) | ||
|
||
val calendar = Calendar.getInstance() | ||
calendar.timeInMillis = date * 1000L | ||
val calendarDate: Date = calendar.time | ||
val dateFormat = DateFormat.getMediumDateFormat(context) | ||
val formattedDate = dateFormat.format(calendarDate) | ||
|
||
emit(Image(id, name, uri, path, bucketId, bucketName, date = formattedDate)) | ||
} | ||
} | ||
}.flowOn(Dispatchers.IO) | ||
} |
15 changes: 15 additions & 0 deletions
15
app/src/main/java/fr/free/nrw/commons/customselector/domain/ImageRepository.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package fr.free.nrw.commons.customselector.domain | ||
|
||
import fr.free.nrw.commons.customselector.domain.model.Image | ||
import kotlinx.coroutines.flow.Flow | ||
|
||
interface ImageRepository { | ||
|
||
suspend fun getImagesFromDevice(): Flow<Image> | ||
|
||
suspend fun markAsNotForUpload(imageSHA: String) | ||
|
||
suspend fun unmarkAsNotForUpload(imageSHA: String) | ||
|
||
suspend fun isNotForUpload(imageSHA: String): Boolean | ||
} |
2 changes: 1 addition & 1 deletion
2
...ns/customselector/model/CallbackStatus.kt → ...omselector/domain/model/CallbackStatus.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...rw/commons/customselector/model/Folder.kt → ...ons/customselector/domain/model/Folder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...nrw/commons/customselector/model/Image.kt → ...mons/customselector/domain/model/Image.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...rw/commons/customselector/model/Result.kt → ...ons/customselector/domain/model/Result.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
app/src/main/java/fr/free/nrw/commons/customselector/domain/use_case/ImageUseCase.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package fr.free.nrw.commons.customselector.domain.use_case | ||
|
||
import android.content.Context | ||
import android.net.Uri | ||
import androidx.exifinterface.media.ExifInterface | ||
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader | ||
import fr.free.nrw.commons.filepicker.PickedFiles | ||
import fr.free.nrw.commons.media.MediaClient | ||
import fr.free.nrw.commons.upload.FileProcessor | ||
import fr.free.nrw.commons.upload.FileUtilsWrapper | ||
import kotlinx.coroutines.CoroutineDispatcher | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.withContext | ||
import okio.FileNotFoundException | ||
import timber.log.Timber | ||
import java.io.IOException | ||
import java.net.UnknownHostException | ||
import javax.inject.Inject | ||
|
||
class ImageUseCase @Inject constructor( | ||
private val fileUtilsWrapper: FileUtilsWrapper, | ||
private val fileProcessor: FileProcessor, | ||
private val mediaClient: MediaClient, | ||
private val context: Context | ||
) { | ||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO | ||
|
||
/** | ||
* Retrieves the SHA1 hash of an image from its URI. | ||
* | ||
* @param uri The URI of the image. | ||
* @return The SHA1 hash of the image, or an empty string if the image is not found. | ||
*/ | ||
suspend fun getImageSHA1(uri: Uri): String = withContext(ioDispatcher) { | ||
try { | ||
val inputStream = context.contentResolver.openInputStream(uri) | ||
fileUtilsWrapper.getSHA1(inputStream) | ||
} catch (e: FileNotFoundException) { | ||
Timber.e(e) | ||
"" | ||
} | ||
} | ||
|
||
/** | ||
* Generates a modified SHA1 hash of an image after redacting sensitive EXIF tags. | ||
* | ||
* @param imageUri The URI of the image to process. | ||
* @return The modified SHA1 hash of the image. | ||
*/ | ||
suspend fun generateModifiedSHA1(imageUri: Uri): String = withContext(ioDispatcher) { | ||
val uploadableFile = PickedFiles.pickedExistingPicture(context, imageUri) | ||
val exifInterface: ExifInterface? = try { | ||
ExifInterface(uploadableFile.file!!) | ||
} catch (e: IOException) { | ||
Timber.e(e) | ||
null | ||
} | ||
fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact()) | ||
|
||
val sha1 = fileUtilsWrapper.getSHA1( | ||
fileUtilsWrapper.getFileInputStream(uploadableFile.filePath)) | ||
uploadableFile.file.delete() | ||
sha1 | ||
} | ||
|
||
/** | ||
* Checks whether a file with the given SHA1 hash exists on Wikimedia Commons. | ||
* | ||
* @param sha1 The SHA1 hash of the file to check. | ||
* @return An ImageLoader.Result indicating the existence of the file on Commons. | ||
*/ | ||
suspend fun checkWhetherFileExistsOnCommonsUsingSHA1( | ||
sha1: String | ||
): ImageLoader.Result = withContext(ioDispatcher) { | ||
return@withContext try { | ||
if (mediaClient.checkFileExistsUsingSha(sha1).blockingGet()) { | ||
ImageLoader.Result.TRUE | ||
} else { | ||
ImageLoader.Result.FALSE | ||
} | ||
} catch (e: UnknownHostException) { | ||
Timber.e(e, "Network Connection Error") | ||
ImageLoader.Result.ERROR | ||
} catch (e: Exception) { | ||
e.printStackTrace() | ||
ImageLoader.Result.ERROR | ||
} | ||
} | ||
} |
4 changes: 2 additions & 2 deletions
4
app/src/main/java/fr/free/nrw/commons/customselector/helper/ImageHelper.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageLoaderListener.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
app/src/main/java/fr/free/nrw/commons/customselector/listeners/ImageSelectListener.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
app/src/main/java/fr/free/nrw/commons/customselector/listeners/PassDataListener.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a
// TODO:
comment - we also use Room in the app, and it supports returning aFlow
from a query. Eventually migrating this query to Room and returning the Flow directly would simplify this code, and for now, aTODO
comment will serve as a nice breadcrumb trail toward a better overall architecture.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for reviewing @psh. I doubt how Room can help fetch images from phone storage.