diff --git a/filepicker/build.gradle b/filepicker/build.gradle index 3a4215d..24644bb 100644 --- a/filepicker/build.gradle +++ b/filepicker/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.library' android { + compileSdkVersion 29 buildToolsVersion '29.0.2' defaultConfig { @@ -18,6 +19,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + } dependencies { @@ -29,4 +31,10 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.2-alpha01' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.2-alpha01' + + + implementation 'io.reactivex.rxjava2:rxjava:2.2.11' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation 'com.github.doctoror.rxcursorloader:library:2.1.2' + } diff --git a/filepicker/src/main/java/com/jaiselrahman/filepicker/activity/FilePickerActivity.java b/filepicker/src/main/java/com/jaiselrahman/filepicker/activity/FilePickerActivity.java index 87bbb4c..072a930 100644 --- a/filepicker/src/main/java/com/jaiselrahman/filepicker/activity/FilePickerActivity.java +++ b/filepicker/src/main/java/com/jaiselrahman/filepicker/activity/FilePickerActivity.java @@ -28,6 +28,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.webkit.MimeTypeMap; @@ -53,8 +54,15 @@ import java.io.File; import java.util.ArrayList; -public class FilePickerActivity extends AppCompatActivity - implements OnSelectionListener, OnCameraClickListener { +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; + +public class FilePickerActivity extends AppCompatActivity implements OnSelectionListener, OnCameraClickListener { + public static final String MEDIA_FILES = "MEDIA_FILES"; public static final String SELECTED_MEDIA_FILES = "SELECTED_MEDIA_FILES"; public static final String CONFIGS = "CONFIGS"; @@ -103,8 +111,7 @@ protected void onCreate(Bundle savedInstanceState) { setSupportActionBar(toolbar); int spanCount; - if (getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE) { + if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { spanCount = configs.getLandscapeSpanCount(); } else { spanCount = configs.getPortraitSpanCount(); @@ -118,6 +125,7 @@ protected void onCreate(Bundle savedInstanceState) { } boolean isSingleChoice = configs.isSingleChoiceMode(); + fileGalleryAdapter = new FileGalleryAdapter(this, mediaFiles, imageSize, configs.isImageCaptureEnabled(), configs.isVideoCaptureEnabled()); @@ -134,51 +142,68 @@ protected void onCreate(Bundle savedInstanceState) { recyclerView.addItemDecoration(new DividerItemDecoration(getResources().getDimensionPixelSize(R.dimen.grid_spacing), spanCount)); recyclerView.setItemAnimator(null); - if (requestPermission(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_WRITE_PERMISSION)) { - loadFiles(false); - } - maxCount = configs.getMaxSelection(); if (maxCount > 0) { setTitle(getResources().getString(R.string.selection_count, fileGalleryAdapter.getSelectedItemCount(), maxCount)); } } + @Override + protected void onResume() { + super.onResume(); + + if (requestPermission(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_WRITE_PERMISSION)) { + loadFiles(); + } + } + private boolean useDocumentUi() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && configs.isShowFiles() && !(configs.isShowImages() || configs.isShowVideos() || configs.isShowAudios()); } - private void loadFiles(boolean restart) { - FileLoader.loadFiles(this, new FileResultCallback() { + private void loadFiles() { + FileLoader fileLoader = new FileLoader(this, configs); + fileLoader.loadFiles(new Consumer>() { @Override - public void onResult(ArrayList filesResults) { - if (filesResults != null) { - mediaFiles.clear(); - mediaFiles.addAll(filesResults); - fileGalleryAdapter.notifyDataSetChanged(); - } + public void accept(ArrayList mediaFiles) { + FilePickerActivity.this.mediaFiles.clear(); + FilePickerActivity.this.mediaFiles.addAll(mediaFiles); + fileGalleryAdapter.notifyDataSetChanged(); } - }, configs, restart); + }); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == REQUEST_WRITE_PERMISSION) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - loadFiles(false); - } else { - Toast.makeText(this, R.string.permission_not_given, Toast.LENGTH_SHORT).show(); - finish(); - } - } else if (requestCode == REQUEST_CAMERA_PERMISSION_FOR_CAMERA || requestCode == REQUEST_CAMERA_PERMISSION_FOR_VIDEO) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - fileGalleryAdapter.openCamera(requestCode == REQUEST_CAMERA_PERMISSION_FOR_VIDEO); - } else { - Toast.makeText(this, R.string.permission_not_given, Toast.LENGTH_SHORT).show(); - } + + switch (requestCode) { + + case REQUEST_WRITE_PERMISSION: + if (grantResults.length > 0) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + loadFiles(); + } else { + Toast.makeText(this, R.string.permission_not_given, Toast.LENGTH_SHORT).show(); + finish(); + } + } + return; + + case REQUEST_CAMERA_PERMISSION_FOR_CAMERA: + case REQUEST_CAMERA_PERMISSION_FOR_VIDEO: + if (grantResults.length > 0) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + fileGalleryAdapter.openCamera(requestCode == REQUEST_CAMERA_PERMISSION_FOR_VIDEO); + } else { + Toast.makeText(this, R.string.permission_not_given, Toast.LENGTH_SHORT).show(); + } + } + return; + + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); } } @@ -197,15 +222,14 @@ public void onScanCompleted(String path, final Uri uri) { runOnUiThread(new Runnable() { @Override public void run() { - loadFiles(true); + loadFiles(); } }); } } }); } else { - getContentResolver().delete(fileGalleryAdapter.getLastCapturedUri(), - null, null); + getContentResolver().delete(fileGalleryAdapter.getLastCapturedUri(), null, null); } } else if (requestCode == REQUEST_DOCUMENT) { ContentResolver contentResolver = getContentResolver(); diff --git a/filepicker/src/main/java/com/jaiselrahman/filepicker/loader/FileLoader.java b/filepicker/src/main/java/com/jaiselrahman/filepicker/loader/FileLoader.java index dd5077d..90e76ba 100644 --- a/filepicker/src/main/java/com/jaiselrahman/filepicker/loader/FileLoader.java +++ b/filepicker/src/main/java/com/jaiselrahman/filepicker/loader/FileLoader.java @@ -17,18 +17,21 @@ package com.jaiselrahman.filepicker.loader; import android.content.ContentResolver; +import android.content.ContentUris; import android.content.Context; import android.content.CursorLoader; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.provider.MediaStore; +import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; +import com.doctoror.rxcursorloader.RxCursorLoader; import com.jaiselrahman.filepicker.config.Configurations; import com.jaiselrahman.filepicker.model.MediaFile; import com.jaiselrahman.filepicker.utils.FileUtils; @@ -38,13 +41,32 @@ import java.util.Arrays; import java.util.List; +import io.reactivex.BackpressureStrategy; +import io.reactivex.FlowableSubscriber; +import io.reactivex.Observer; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.functions.Function; +import io.reactivex.schedulers.Schedulers; + +import static android.provider.BaseColumns._ID; +import static android.provider.MediaStore.Audio.AlbumColumns.ALBUM_ID; +import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE; import static android.provider.MediaStore.Images.ImageColumns.BUCKET_ID; +import static android.provider.MediaStore.MediaColumns.BUCKET_DISPLAY_NAME; import static android.provider.MediaStore.MediaColumns.DATA; import static android.provider.MediaStore.MediaColumns.DATE_ADDED; +import static android.provider.MediaStore.MediaColumns.DISPLAY_NAME; +import static android.provider.MediaStore.MediaColumns.DURATION; +import static android.provider.MediaStore.MediaColumns.HEIGHT; import static android.provider.MediaStore.MediaColumns.MIME_TYPE; +import static android.provider.MediaStore.MediaColumns.SIZE; +import static android.provider.MediaStore.MediaColumns.WIDTH; import static com.jaiselrahman.filepicker.activity.FilePickerActivity.TAG; -public class FileLoader extends CursorLoader { +public class FileLoader { private static final ArrayList ImageSelectionArgs = new ArrayList<>(); private static final ArrayList AudioSelectionArgs = new ArrayList<>(); private static final ArrayList VideoSelectionArgs = new ArrayList<>(); @@ -62,17 +84,20 @@ public class FileLoader extends CursorLoader { MediaStore.Video.Media.DURATION ); - static { - ImageSelectionArgs.addAll(Arrays.asList("image/jpeg", "image/png", "image/jpg", "image/gif")); - AudioSelectionArgs.addAll(Arrays.asList("audio/mpeg", "audio/mp3", "audio/x-ms-wma", "audio/x-wav", "audio/amr", "audio/3gp")); - VideoSelectionArgs.addAll(Arrays.asList("video/mpeg", "video/mp4")); - } private Configurations configs; + private Context context; + private CompositeDisposable disposable = new CompositeDisposable(); + private final RxCursorLoader.Query query; - FileLoader(Context context, @NonNull Configurations configs) { - super(context); + public FileLoader(Context context, @NonNull Configurations configs) { + this.context = context; this.configs = configs; + + ImageSelectionArgs.addAll(Arrays.asList("image/jpeg", "image/png", "image/jpg", "image/gif")); + AudioSelectionArgs.addAll(Arrays.asList("audio/mpeg", "audio/mp3", "audio/x-ms-wma", "audio/x-wav", "audio/amr", "audio/3gp")); + VideoSelectionArgs.addAll(Arrays.asList("video/mpeg", "video/mp4")); + ArrayList selectionArgs = new ArrayList<>(); StringBuilder selectionBuilder = new StringBuilder(); @@ -83,6 +108,7 @@ public class FileLoader extends CursorLoader { selectionArgs.add(rootPath + "%"); } + if (configs.isShowImages()) selectionArgs.addAll(ImageSelectionArgs); if (configs.isShowAudios()) @@ -128,28 +154,20 @@ public class FileLoader extends CursorLoader { selectionArgs.add(folders.get(i) + "%"); } } + selectionBuilder.append(")"); - List projection = new ArrayList<>(FILE_PROJECTION); + this.query = new RxCursorLoader.Query.Builder() + .setContentUri(getContentUri(configs)) + .setProjection(FILE_PROJECTION.toArray(new String[0])) + .setSelection(selectionBuilder.toString().concat(" OFFSET ?")) + .setSelectionArgs(selectionArgs.toArray(new String[0])) + .create(); - if (selectionBuilder.length() != 0) { - if (canUseAlbumId(configs)) { - projection.add(MediaStore.Audio.AudioColumns.ALBUM_ID); - } - if (canUseMediaType(configs)) { - projection.add(MediaStore.Files.FileColumns.MEDIA_TYPE); - } - setProjection(projection.toArray(new String[0])); - setUri(getContentUri(configs)); - setSortOrder(DATE_ADDED + " DESC"); - setSelection(selectionBuilder.toString()); - setSelectionArgs(selectionArgs.toArray(new String[0])); - } } static Uri getContentUri(Configurations configs) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && - (configs.isShowAudios() && !(configs.isShowFiles() || configs.isShowImages() || configs.isShowVideos()))) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && (configs.isShowAudios() && !(configs.isShowFiles() || configs.isShowImages() || configs.isShowVideos()))) { return MediaStore.Audio.Media.getContentUri("external"); } else { return MediaStore.Files.getContentUri("external"); @@ -176,7 +194,7 @@ private List getFoldersToIgnore() { selection = BUCKET_ID + " IS NOT NULL"; } String sortOrder = DATA + " ASC"; - Cursor cursor = getContext().getContentResolver().query(uri, projection, selection, null, sortOrder); + Cursor cursor = context.getContentResolver().query(uri, projection, selection, null, sortOrder); if (cursor == null) { Log.e(TAG, "IgnoreFolders Cursor NULL"); return new ArrayList<>(); @@ -206,17 +224,35 @@ private boolean isExcluded(String path, List ignoredPaths) { return false; } - public static void loadFiles(FragmentActivity activity, FileResultCallback fileResultCallback, Configurations configs, boolean restart) { - if (configs.isShowFiles() || configs.isShowVideos() || configs.isShowAudios() || configs.isShowImages()) { - FileLoaderCallback fileLoaderCallBack = new FileLoaderCallback(activity, fileResultCallback, configs); - if (!restart) { - activity.getLoaderManager().initLoader(0, null, fileLoaderCallBack); - } else { - activity.getLoaderManager().restartLoader(0, null, fileLoaderCallBack); - } - } else { - fileResultCallback.onResult(null); - } + public void loadFiles(Consumer> consumer) { + + disposable.add(RxCursorLoader + .single(context.getContentResolver(), query) + .map(new Function>() { + @Override + public ArrayList apply(Cursor cursor) { + ArrayList mediaFiles = new ArrayList<>(); + if (cursor.moveToFirst()) + do { + MediaFile mediaFile = asMediaFile(cursor, configs, null); + if (mediaFile != null) { + mediaFiles.add(mediaFile); + } + } while (cursor.moveToNext()); + + mediaFiles.addAll(mediaFiles); + return mediaFiles; + } + }) + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(consumer, new Consumer() { + @Override + public void accept(Throwable throwable) { + Log.e("FilePickerError", throwable.getMessage()); + } + })); + } @Nullable @@ -227,4 +263,76 @@ public static MediaFile asMediaFile(ContentResolver contentResolver, Uri uri, Co } return null; } + + private MediaFile asMediaFile(@NonNull Cursor data, Configurations configs, @Nullable Uri uri) { + MediaFile mediaFile = new MediaFile(); + mediaFile.setPath(data.getString(data.getColumnIndex(DATA))); + + long size = data.getLong(data.getColumnIndex(SIZE)); + //noinspection deprecation + if (size == 0 && mediaFile.getPath() != null) { + //Check if File size is really zero + size = new java.io.File(data.getString(data.getColumnIndex(DATA))).length(); + if (size <= 0 && configs.isSkipZeroSizeFiles()) + return null; + } + mediaFile.setSize(size); + + mediaFile.setId(data.getLong(data.getColumnIndex(_ID))); + mediaFile.setName(data.getString(data.getColumnIndex(DISPLAY_NAME))); + mediaFile.setPath(data.getString(data.getColumnIndex(DATA))); + mediaFile.setDate(data.getLong(data.getColumnIndex(DATE_ADDED))); + mediaFile.setMimeType(data.getString(data.getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE))); + mediaFile.setBucketId(data.getString(data.getColumnIndex(BUCKET_ID))); + mediaFile.setBucketName(data.getString(data.getColumnIndex(BUCKET_DISPLAY_NAME))); + mediaFile.setUri(uri != null ? uri : ContentUris.withAppendedId(FileLoader.getContentUri(configs), mediaFile.getId())); + mediaFile.setDuration(data.getLong(data.getColumnIndex(DURATION))); + + if (TextUtils.isEmpty(mediaFile.getName())) { + //noinspection deprecation + String path = mediaFile.getPath() != null ? mediaFile.getPath() : ""; + mediaFile.setName(path.substring(path.lastIndexOf('/') + 1)); + } + + int mediaTypeIndex = data.getColumnIndex(MEDIA_TYPE); + if (mediaTypeIndex >= 0) { + mediaFile.setMediaType(data.getInt(mediaTypeIndex)); + } + + if ((mediaFile.getMediaType() == MediaFile.TYPE_FILE + || mediaFile.getMediaType() > MediaFile.TYPE_MAX) + && mediaFile.getMimeType() != null) { + //Double check correct MediaType + mediaFile.setMediaType(getMediaType(mediaFile.getMimeType())); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + mediaFile.setHeight(data.getLong(data.getColumnIndex(HEIGHT))); + mediaFile.setWidth(data.getLong(data.getColumnIndex(WIDTH))); + } + + int albumIdIndex = data.getColumnIndex(ALBUM_ID); + if (albumIdIndex >= 0) { + int albumId = data.getInt(albumIdIndex); + if (albumId >= 0) { + mediaFile.setThumbnail(ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), albumId)); + } + } + return mediaFile; + } + + private static @MediaFile.Type + int getMediaType(String mime) { + if (mime.startsWith("image/")) { + return MediaFile.TYPE_IMAGE; + } else if (mime.startsWith("video/")) { + return MediaFile.TYPE_VIDEO; + } else if (mime.startsWith("audio/")) { + return MediaFile.TYPE_AUDIO; + } else { + return MediaFile.TYPE_FILE; + } + } + + } diff --git a/filepicker/src/main/java/com/jaiselrahman/filepicker/loader/FileLoaderCallback.java b/filepicker/src/main/java/com/jaiselrahman/filepicker/loader/FileLoaderCallback.java index f7f3413..73c9da4 100644 --- a/filepicker/src/main/java/com/jaiselrahman/filepicker/loader/FileLoaderCallback.java +++ b/filepicker/src/main/java/com/jaiselrahman/filepicker/loader/FileLoaderCallback.java @@ -63,7 +63,7 @@ class FileLoaderCallback implements LoaderManager.LoaderCallbacks { @Override public Loader onCreateLoader(int id, Bundle args) { - return new FileLoader(context, configs); + return null; } @Override @@ -147,8 +147,7 @@ static MediaFile asMediaFile(@NonNull Cursor data, Configurations configs, @Null if (albumIdIndex >= 0) { int albumId = data.getInt(albumIdIndex); if (albumId >= 0) { - mediaFile.setThumbnail(ContentUris - .withAppendedId(Uri.parse("content://media/external/audio/albumart"), albumId)); + mediaFile.setThumbnail(ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), albumId)); } } return mediaFile; diff --git a/filepicker/src/main/res/layout/filegallery_shimmer_item.xml b/filepicker/src/main/res/layout/filegallery_shimmer_item.xml new file mode 100644 index 0000000..02b3e96 --- /dev/null +++ b/filepicker/src/main/res/layout/filegallery_shimmer_item.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/filepicker/src/main/res/values-ar/strings.xml b/filepicker/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000..738e362 --- /dev/null +++ b/filepicker/src/main/res/values-ar/strings.xml @@ -0,0 +1,8 @@ + + + FilePicker + تاكيد + صورة مصغره + افتح الكاميرا + Permission not given + \ No newline at end of file diff --git a/filepicker/src/main/res/values/strings.xml b/filepicker/src/main/res/values/strings.xml index df308e3..523ec76 100644 --- a/filepicker/src/main/res/values/strings.xml +++ b/filepicker/src/main/res/values/strings.xml @@ -3,6 +3,6 @@ File Thumbnail Open Camera Done - %1$d/%2$d + %1$d/%2$d Permission not given