fedilab-Android-App/app/src/main/java/app/fedilab/android/imageeditor/FileSaveHelper.java

186 lines
6.9 KiB
Java

package app.fedilab.android.imageeditor;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.OnLifecycleEvent;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* General contract of this class is to
* create a file on a device.
* </br>
* How to Use it-
* Call {@linkplain FileSaveHelper#createFile(String, OnFileCreateResult)}
* if file is created you would receive it's file path and Uri
* and after you are done with File call {@linkplain FileSaveHelper#notifyThatFileIsNowPubliclyAvailable(ContentResolver)}
* </br>
* Remember! in order to shutdown executor call {@linkplain FileSaveHelper#addObserver(LifecycleOwner)} or
* create object with the {@linkplain FileSaveHelper#FileSaveHelper(AppCompatActivity)}
*/
public class FileSaveHelper implements LifecycleObserver {
private final ContentResolver mContentResolver;
private final ExecutorService executor;
private final MutableLiveData<FileMeta> fileCreatedResult;
private OnFileCreateResult resultListener;
private final Observer<FileMeta> observer = fileMeta -> {
if (resultListener != null) {
resultListener.onFileCreateResult(fileMeta.isCreated,
fileMeta.filePath,
fileMeta.error,
fileMeta.uri);
}
};
public FileSaveHelper(ContentResolver contentResolver) {
mContentResolver = contentResolver;
executor = Executors.newSingleThreadExecutor();
fileCreatedResult = new MutableLiveData<>();
}
public FileSaveHelper(AppCompatActivity activity) {
this(activity.getContentResolver());
addObserver(activity);
}
public static boolean isSdkHigherThan28() {
return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q);
}
private void addObserver(LifecycleOwner lifecycleOwner) {
fileCreatedResult.observe(lifecycleOwner, observer);
lifecycleOwner.getLifecycle().addObserver(this);
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void release() {
if (null != executor) {
executor.shutdownNow();
}
}
/**
* The effects of this method are
* 1- insert new Image File data in MediaStore.Images column
* 2- create File on Disk.
*
* @param fileNameToSave fileName
* @param listener result listener
*/
public void createFile(final String fileNameToSave, OnFileCreateResult listener) {
this.resultListener = listener;
executor.submit(() -> {
Cursor cursor = null;
String filePath;
try {
final ContentValues newImageDetails = new ContentValues();
Uri imageCollection = buildUriCollection(newImageDetails);
final Uri editedImageUri = getEditedImageUri(fileNameToSave, newImageDetails, imageCollection);
filePath = getFilePath(cursor, editedImageUri);
updateResult(true, filePath, null, editedImageUri, newImageDetails);
} catch (final Exception ex) {
ex.printStackTrace();
updateResult(false, null, ex.getMessage(), null, null);
} finally {
if (cursor != null) {
cursor.close();
}
}
});
}
private String getFilePath(Cursor cursor, Uri editedImageUri) {
String[] proj = {MediaStore.Images.Media.DATA};
cursor = mContentResolver.query(editedImageUri, proj, null, null, null);
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
return cursor.getString(column_index);
}
private Uri getEditedImageUri(String fileNameToSave, ContentValues newImageDetails, Uri imageCollection) throws IOException {
newImageDetails.put(MediaStore.Images.Media.DISPLAY_NAME, fileNameToSave);
final Uri editedImageUri = mContentResolver.insert(imageCollection, newImageDetails);
final OutputStream outputStream = mContentResolver.openOutputStream(editedImageUri);
outputStream.close();
return editedImageUri;
}
@SuppressLint("InlinedApi")
private Uri buildUriCollection(ContentValues newImageDetails) {
Uri imageCollection;
if (isSdkHigherThan28()) {
imageCollection = MediaStore.Images.Media.getContentUri(
MediaStore.VOLUME_EXTERNAL_PRIMARY
);
newImageDetails.put(MediaStore.Images.Media.IS_PENDING, 1);
} else {
imageCollection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
return imageCollection;
}
@SuppressLint("InlinedApi")
public void notifyThatFileIsNowPubliclyAvailable(ContentResolver contentResolver) {
if (isSdkHigherThan28()) {
executor.submit(() -> {
FileMeta value = fileCreatedResult.getValue();
if (value != null) {
value.imageDetails.clear();
value.imageDetails.put(MediaStore.Images.Media.IS_PENDING, 0);
contentResolver.update(value.uri, value.imageDetails, null, null);
}
});
}
}
private void updateResult(boolean result, String filePath, String error, Uri uri, ContentValues newImageDetails) {
fileCreatedResult.postValue(new FileMeta(result, filePath, uri, error, newImageDetails));
}
public interface OnFileCreateResult {
/**
* @param created whether file creation is success or failure
* @param filePath filepath on disk. null in case of failure
* @param error in case file creation is failed . it would represent the cause
* @param Uri Uri to the newly created file. null in case of failure
*/
void onFileCreateResult(boolean created, String filePath, String error, Uri Uri);
}
private static class FileMeta {
public ContentValues imageDetails;
public boolean isCreated;
public String filePath;
public Uri uri;
public String error;
public FileMeta(boolean isCreated, String filePath,
Uri uri, String error,
ContentValues newImageDetails) {
this.isCreated = isCreated;
this.filePath = filePath;
this.uri = uri;
this.error = error;
this.imageDetails = newImageDetails;
}
}
}