mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
[GSOC] Added Image Fetch (#4449)
* Added basic Fetch * added permission request * Folder count rectified * Loaded thumbnail * disabled overlay * Added sha1 function * Documented the code
This commit is contained in:
parent
2dbda3a488
commit
a120030a44
12 changed files with 240 additions and 40 deletions
|
|
@ -142,6 +142,10 @@ dependencies {
|
|||
def work_version = "2.4.0"
|
||||
// Kotlin + coroutines
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
|
||||
//Glide
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import androidx.annotation.NonNull;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity;
|
||||
import fr.free.nrw.commons.filepicker.DefaultCallback;
|
||||
import fr.free.nrw.commons.filepicker.FilePicker;
|
||||
import fr.free.nrw.commons.filepicker.FilePicker.ImageSource;
|
||||
|
|
@ -58,6 +59,25 @@ public class ContributionController {
|
|||
initiateGalleryUpload(activity, allowMultipleUploads);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate gallery picker with permission
|
||||
*/
|
||||
public void initiateCustomGalleryPickWithPermission(final Activity activity) {
|
||||
boolean useExtStorage = defaultKvStore.getBoolean("useExternalStorage", true);
|
||||
Intent intent = new Intent(activity,CustomSelectorActivity.class);
|
||||
if (!useExtStorage) {
|
||||
activity.startActivity(intent);
|
||||
return;
|
||||
}
|
||||
|
||||
PermissionUtils.checkPermissionsAndPerformAction(activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
() -> activity.startActivity(intent),
|
||||
R.string.storage_permission_title,
|
||||
R.string.write_storage_permission_rationale);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Open chooser for gallery uploads
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -271,8 +271,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
|
||||
@OnClick(R.id.fab_custom_gallery)
|
||||
void launchCustomSelector(){
|
||||
Intent intent = new Intent(getActivity(), CustomSelectorActivity.class);
|
||||
startActivity(intent);
|
||||
controller.initiateCustomGalleryPickWithPermission(getActivity());
|
||||
}
|
||||
|
||||
private void animateFAB(final boolean isFabOpen) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
package fr.free.nrw.commons.customselector.helper
|
||||
|
||||
import fr.free.nrw.commons.customselector.model.Folder
|
||||
import fr.free.nrw.commons.customselector.model.Image
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
/**
|
||||
* Image Helper object, includes all the static functions required by custom selector
|
||||
*/
|
||||
|
||||
object ImageHelper {
|
||||
|
||||
/**
|
||||
* Returns the list of folders from given image list.
|
||||
*/
|
||||
fun folderListFromImages(images: List<Image>): List<Folder> {
|
||||
val folderMap: MutableMap<Long, Folder> = LinkedHashMap()
|
||||
for (image in images) {
|
||||
val bucketId = image.bucketId
|
||||
val bucketName = image.bucketName
|
||||
var folder = folderMap[bucketId]
|
||||
if (folder == null) {
|
||||
folder = Folder(bucketId, bucketName)
|
||||
folderMap[bucketId] = folder
|
||||
}
|
||||
folder.images.add(image)
|
||||
}
|
||||
return ArrayList(folderMap.values)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the images based on the given bucketId (folder)
|
||||
*/
|
||||
fun filterImages(images: ArrayList<Image>, bukketId: Long?): ArrayList<Image> {
|
||||
if (bukketId == null) return images
|
||||
|
||||
val filteredImages = arrayListOf<Image>()
|
||||
for (image in images) {
|
||||
if (image.bucketId == bukketId) {
|
||||
filteredImages.add(image)
|
||||
}
|
||||
}
|
||||
return filteredImages
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the file sha1 from file input stream.
|
||||
*/
|
||||
fun generateSHA1(`is`: InputStream): String {
|
||||
val digest: MessageDigest = try {
|
||||
MessageDigest.getInstance("SHA1")
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Timber.e(e, "Exception while getting Digest")
|
||||
return ""
|
||||
}
|
||||
val buffer = ByteArray(8192)
|
||||
var read: Int
|
||||
return try {
|
||||
while (`is`.read(buffer).also { read = it } > 0) {
|
||||
digest.update(buffer, 0, read)
|
||||
}
|
||||
val md5sum = digest.digest()
|
||||
val bigInt = BigInteger(1, md5sum)
|
||||
var output = bigInt.toString(16)
|
||||
output = String.format("%40s", output).replace(' ', '0')
|
||||
Timber.i("File SHA1: %s", output)
|
||||
output
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "IO Exception")
|
||||
""
|
||||
} finally {
|
||||
try {
|
||||
`is`.close()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "Exception on closing input stream")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file input stream from the file path.
|
||||
*/
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun getFileInputStream(filePath: String?): InputStream {
|
||||
return FileInputStream(filePath)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
|
||||
import fr.free.nrw.commons.customselector.model.Folder
|
||||
|
|
@ -49,8 +50,9 @@ class FolderAdapter(
|
|||
val folder = folders[position]
|
||||
val count = folder.images.size
|
||||
val previewImage = folder.images[0]
|
||||
Glide.with(context).load(previewImage.uri).into(holder.image)
|
||||
holder.name.text = folder.name
|
||||
holder.count.text= count.toString()
|
||||
holder.count.text = count.toString()
|
||||
holder.itemView.setOnClickListener{
|
||||
itemClickListener.onFolderClick(folder)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import android.widget.TextView
|
|||
import androidx.constraintlayout.widget.Group
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||
import fr.free.nrw.commons.customselector.model.Image
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ class ImageAdapter(
|
|||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||
val image=images[position]
|
||||
// todo load image thumbnail, set selected view.
|
||||
Glide.with(context).load(image.uri).into(holder.image)
|
||||
holder.itemView.setOnClickListener {
|
||||
selectOrRemoveImage(image, position)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
package fr.free.nrw.commons.customselector.ui.selector
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener
|
||||
import fr.free.nrw.commons.customselector.model.CallbackStatus
|
||||
import fr.free.nrw.commons.customselector.model.Image
|
||||
import fr.free.nrw.commons.customselector.model.Result
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
|
||||
class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() {
|
||||
class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
/**
|
||||
* Result Live Data
|
||||
|
|
@ -20,9 +26,8 @@ class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFil
|
|||
*/
|
||||
fun fetchImages() {
|
||||
result.postValue(Result(CallbackStatus.FETCHING, arrayListOf()))
|
||||
imageFileLoader.abortLoadImage()
|
||||
scope.cancel()
|
||||
imageFileLoader.loadDeviceImages(object: ImageLoaderListener {
|
||||
|
||||
override fun onImageLoaded(images: ArrayList<Image>) {
|
||||
result.postValue(Result(CallbackStatus.SUCCESS, images))
|
||||
}
|
||||
|
|
@ -30,7 +35,14 @@ class CustomSelectorViewModel(val context: Context,var imageFileLoader: ImageFil
|
|||
override fun onFailed(throwable: Throwable) {
|
||||
result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf()))
|
||||
}
|
||||
},scope)
|
||||
}
|
||||
|
||||
})
|
||||
/**
|
||||
* Clear the coroutine task linked with context.
|
||||
*/
|
||||
override fun onCleared() {
|
||||
scope.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import androidx.lifecycle.Observer
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
||||
import fr.free.nrw.commons.customselector.model.Result
|
||||
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
|
||||
import fr.free.nrw.commons.customselector.model.CallbackStatus
|
||||
|
|
@ -29,7 +30,7 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
|||
* View Model Factory.
|
||||
*/
|
||||
var customSelectorViewModelFactory: CustomSelectorViewModelFactory? = null
|
||||
@Inject set
|
||||
@Inject set
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -67,7 +68,7 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
|||
*/
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val root = inflater.inflate(R.layout.fragment_custom_selector, container, false)
|
||||
folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener)
|
||||
folderAdapter = FolderAdapter(activity!!, activity as FolderClickListener)
|
||||
gridLayoutManager = GridLayoutManager(context, columnCount())
|
||||
with(root.selector_rv){
|
||||
this.layoutManager = gridLayoutManager
|
||||
|
|
@ -87,10 +88,7 @@ class FolderFragment : CommonsDaggerSupportFragment() {
|
|||
*/
|
||||
private fun handleResult(result: Result) {
|
||||
if(result.status is CallbackStatus.SUCCESS){
|
||||
val folders = arrayListOf<Folder>()
|
||||
for( i in 1..12) {
|
||||
folders.add(Folder(i.toLong(), "Folder$i",result.images))
|
||||
}
|
||||
val folders = ImageHelper.folderListFromImages(result.images)
|
||||
folderAdapter.init(folders)
|
||||
folderAdapter.notifyDataSetChanged()
|
||||
selector_rv.visibility = View.VISIBLE
|
||||
|
|
|
|||
|
|
@ -1,26 +1,97 @@
|
|||
package fr.free.nrw.commons.customselector.ui.selector
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import fr.free.nrw.commons.customselector.listeners.ImageLoaderListener
|
||||
import fr.free.nrw.commons.customselector.model.Image
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class ImageFileLoader(val context: Context) {
|
||||
class ImageFileLoader(val context: Context) : CoroutineScope{
|
||||
|
||||
/**
|
||||
* Load Device Images.
|
||||
* Coroutine context for fetching images.
|
||||
*/
|
||||
fun loadDeviceImages(listener: ImageLoaderListener) {
|
||||
var tempImage = Image(0, "temp", Uri.parse("http://www.google.com"), "path", 0, "bucket", "1223")
|
||||
var array: ArrayList<Image> = ArrayList()
|
||||
for(i in 1..100) {
|
||||
array.add(tempImage)
|
||||
}
|
||||
listener.onImageLoaded(array)
|
||||
override val coroutineContext: CoroutineContext = Dispatchers.Main
|
||||
|
||||
// todo load images from device using cursor.
|
||||
/**
|
||||
* Media paramerters required.
|
||||
*/
|
||||
private 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)
|
||||
|
||||
/**
|
||||
* Load Device Images under coroutine.
|
||||
*/
|
||||
fun loadDeviceImages(listener: ImageLoaderListener, scope: CoroutineScope) {
|
||||
launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.IO) {
|
||||
getImages(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load the device images using cursor
|
||||
*/
|
||||
private fun getImages(listener:ImageLoaderListener) {
|
||||
val cursor = context.contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, MediaStore.Images.Media.DATE_ADDED + " DESC")
|
||||
if (cursor == null) {
|
||||
listener.onFailed(NullPointerException())
|
||||
return
|
||||
}
|
||||
|
||||
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 images = arrayListOf<Image>()
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
if (Thread.interrupted()) {
|
||||
listener.onFailed(NullPointerException())
|
||||
return
|
||||
}
|
||||
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 file =
|
||||
if (path == null || path.isEmpty()) {
|
||||
null
|
||||
} else try {
|
||||
File(path)
|
||||
} catch (ignored: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
if (file != null && file.exists()) {
|
||||
if (id != null && name != null && path != null && bucketId != null && bucketName != null) {
|
||||
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||
val image = Image(id, name, uri, path, bucketId, bucketName)
|
||||
images.add(image)
|
||||
}
|
||||
}
|
||||
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
cursor.close()
|
||||
listener.onImageLoaded(images)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Abort loading images.
|
||||
*/
|
||||
|
|
@ -31,7 +102,6 @@ class ImageFileLoader(val context: Context) {
|
|||
/**
|
||||
*
|
||||
* TODO
|
||||
* Runnable Thread for image loading.
|
||||
* Sha1 for image (original image).
|
||||
*
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||
import fr.free.nrw.commons.customselector.model.CallbackStatus
|
||||
import fr.free.nrw.commons.customselector.model.Result
|
||||
|
|
@ -34,7 +34,7 @@ class ImageFragment: CommonsDaggerSupportFragment() {
|
|||
* View model Factory.
|
||||
*/
|
||||
lateinit var customSelectorViewModelFactory: CustomSelectorViewModelFactory
|
||||
@Inject set
|
||||
@Inject set
|
||||
|
||||
/**
|
||||
* Image Adapter for recycle view.
|
||||
|
|
@ -106,7 +106,7 @@ class ImageFragment: CommonsDaggerSupportFragment() {
|
|||
if(result.status is CallbackStatus.SUCCESS){
|
||||
val images = result.images
|
||||
if(images.isNotEmpty()) {
|
||||
imageAdapter.init(images)
|
||||
imageAdapter.init(ImageHelper.filterImages(images,bucketId))
|
||||
selector_rv.visibility = View.VISIBLE
|
||||
}
|
||||
else{
|
||||
|
|
|
|||
|
|
@ -19,30 +19,30 @@
|
|||
android:background="@color/white"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/folder_thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:alpha="0.15"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<View
|
||||
android:id="@+id/album_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0.05" />
|
||||
android:background="@color/black"
|
||||
android:alpha="0.15" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/folder_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#4D000000"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/folder_name"
|
||||
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
|
||||
android:textColor="@color/white"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/dimen_6"
|
||||
|
|
@ -50,18 +50,17 @@
|
|||
android:ellipsize="end"
|
||||
android:padding="@dimen/dimen_6"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/folder_count"
|
||||
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
|
||||
android:textColor="@color/white"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/image_thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
android:id="@+id/selected_group"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="selected_overlay,selected_count"/>
|
||||
|
||||
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
android:id="@+id/uploaded_group"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="uploaded_overlay,uploaded_overlay_icon"/>
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue