diff --git a/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java b/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java index a033473d..c19e677e 100644 --- a/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java +++ b/app/src/main/java/awais/instagrabber/InstaGrabberApplication.java @@ -14,6 +14,7 @@ import java.text.SimpleDateFormat; import java.util.UUID; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.LocaleUtils; import awais.instagrabber.utils.SettingsHelper; import awais.instagrabber.utils.TextUtils; @@ -86,5 +87,6 @@ public final class InstaGrabberApplication extends Application { if (TextUtils.isEmpty(settingsHelper.getString(Constants.DEVICE_UUID))) { settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString()); } + DownloadUtils.init(this); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/activities/CameraActivity.java b/app/src/main/java/awais/instagrabber/activities/CameraActivity.java index 9a76a9be..3172439b 100644 --- a/app/src/main/java/awais/instagrabber/activities/CameraActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/CameraActivity.java @@ -5,12 +5,9 @@ import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.hardware.display.DisplayManager; -import android.media.MediaScannerConnection; -import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; -import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -21,11 +18,13 @@ import androidx.camera.core.ImageCaptureException; import androidx.camera.core.Preview; import androidx.camera.lifecycle.ProcessCameraProvider; import androidx.core.content.ContextCompat; +import androidx.documentfile.provider.DocumentFile; -import com.google.common.io.Files; import com.google.common.util.concurrent.ListenableFuture; -import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.Locale; import java.util.concurrent.ExecutionException; @@ -45,7 +44,7 @@ public class CameraActivity extends BaseLanguageActivity { private ActivityCameraBinding binding; private ImageCapture imageCapture; - private File outputDirectory; + private DocumentFile outputDirectory; private ExecutorService cameraExecutor; private int displayId = -1; @@ -113,7 +112,13 @@ public class CameraActivity extends BaseLanguageActivity { } private void updateUi() { - binding.cameraCaptureButton.setOnClickListener(v -> takePhoto()); + binding.cameraCaptureButton.setOnClickListener(v -> { + try { + takePhoto(); + } catch (FileNotFoundException e) { + Log.e(TAG, "updateUi: ", e); + } + }); // Disable the button until the camera is set up binding.switchCamera.setEnabled(false); // Listener for button used to switch cameras. Only called if the button is enabled @@ -200,37 +205,44 @@ public class CameraActivity extends BaseLanguageActivity { preview.setSurfaceProvider(binding.viewFinder.getSurfaceProvider()); } - private void takePhoto() { + private void takePhoto() throws FileNotFoundException { if (imageCapture == null) return; - final File photoFile = new File(outputDirectory, SIMPLE_DATE_FORMAT.format(System.currentTimeMillis()) + ".jpg"); - final ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(photoFile).build(); + final String extension = "jpg"; + final String fileName = SIMPLE_DATE_FORMAT.format(System.currentTimeMillis()) + "." + extension; + // final File photoFile = new File(outputDirectory, fileName); + final String mimeType = Utils.mimeTypeMap.getMimeTypeFromExtension(extension); + final DocumentFile photoFile = outputDirectory.createFile(mimeType, fileName); + final OutputStream outputStream = getContentResolver().openOutputStream(photoFile.getUri()); + final ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(outputStream).build(); imageCapture.takePicture( outputFileOptions, cameraExecutor, new ImageCapture.OnImageSavedCallback() { @Override public void onImageSaved(@NonNull final ImageCapture.OutputFileResults outputFileResults) { - final Uri uri = Uri.fromFile(photoFile); - //noinspection UnstableApiUsage - final String mimeType = MimeTypeMap.getSingleton() - .getMimeTypeFromExtension(Files.getFileExtension(photoFile.getName())); - MediaScannerConnection.scanFile( - CameraActivity.this, - new String[]{photoFile.getAbsolutePath()}, - new String[]{mimeType}, - (path, uri1) -> { - Log.d(TAG, "onImageSaved: scan complete"); - final Intent intent = new Intent(); - intent.setData(uri1); - setResult(Activity.RESULT_OK, intent); - finish(); - }); - Log.d(TAG, "onImageSaved: " + uri); + if (outputStream != null) { + try { outputStream.close(); } catch (IOException ignored) {} + } + // final Uri uri = Uri.fromFile(photoFile); + // final String mimeType = MimeTypeMap.getSingleton() + // .getMimeTypeFromExtension(Files.getFileExtension(photoFile.getName())); + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, photoFile.getUri())); + Utils.scanDocumentFile(CameraActivity.this, photoFile, (path, uri1) -> { + Log.d(TAG, "onImageSaved: scan complete"); + final Intent intent = new Intent(); + intent.setData(uri1); + setResult(Activity.RESULT_OK, intent); + finish(); + }); + Log.d(TAG, "onImageSaved: " + photoFile.getUri()); } @Override public void onError(@NonNull final ImageCaptureException exception) { Log.e(TAG, "onError: ", exception); + if (outputStream != null) { + try { outputStream.close(); } catch (IOException ignored) {} + } } } ); diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java index c6e1d571..3f71c624 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedGridItemViewHolder.java @@ -102,7 +102,7 @@ public class FeedGridItemViewHolder extends RecyclerView.ViewHolder { binding.typeIcon.setVisibility(View.VISIBLE); binding.typeIcon.setImageResource(typeIconRes); } - final DownloadedCheckerAsyncTask task = new DownloadedCheckerAsyncTask(result -> { + final DownloadedCheckerAsyncTask task = new DownloadedCheckerAsyncTask(itemView.getContext(), result -> { final List checkList = result.get(media.getPk()); if (checkList == null || checkList.isEmpty()) { return; diff --git a/app/src/main/java/awais/instagrabber/asyncs/DownloadedCheckerAsyncTask.java b/app/src/main/java/awais/instagrabber/asyncs/DownloadedCheckerAsyncTask.java index 605bed3f..fd4ad6df 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/DownloadedCheckerAsyncTask.java +++ b/app/src/main/java/awais/instagrabber/asyncs/DownloadedCheckerAsyncTask.java @@ -1,7 +1,9 @@ package awais.instagrabber.asyncs; +import android.content.Context; import android.os.AsyncTask; +import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -12,9 +14,12 @@ import awais.instagrabber.utils.DownloadUtils; public final class DownloadedCheckerAsyncTask extends AsyncTask>> { private static final String TAG = "DownloadedCheckerAsyncTask"; + private final WeakReference context; private final OnCheckResultListener listener; - public DownloadedCheckerAsyncTask(final OnCheckResultListener listener) { + public DownloadedCheckerAsyncTask(final Context context, + final OnCheckResultListener listener) { + this.context = new WeakReference<>(context); this.listener = listener; } @@ -25,7 +30,9 @@ public final class DownloadedCheckerAsyncTask extends AsyncTask> map = new HashMap<>(); for (final Media media : feedModels) { - map.put(media.getPk(), DownloadUtils.checkDownloaded(media)); + final Context context = this.context.get(); + if (context == null) return map; + map.put(media.getPk(), DownloadUtils.checkDownloaded(context, media)); } return map; } diff --git a/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java index 42b0ebd1..2cbd0224 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java @@ -3,7 +3,6 @@ package awais.instagrabber.dialogs; import android.app.Dialog; import android.content.Context; import android.content.Intent; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.DocumentsContract; @@ -214,7 +213,7 @@ public class CreateBackupDialogFragment extends DialogFragment { // Optionally, specify a URI for the directory that should be opened in // the system file picker when your app creates the document. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.fromFile(DownloadUtils.getDownloadDir())); + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, DownloadUtils.getDownloadDir().getUri()); } startActivityForResult(intent, CREATE_FILE_REQUEST_CODE); diff --git a/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java index affe5cba..9df1e552 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java @@ -7,7 +7,6 @@ import android.graphics.Color; import android.graphics.drawable.Animatable; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; -import android.os.Environment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -17,6 +16,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import androidx.documentfile.provider.DocumentFile; import androidx.fragment.app.DialogFragment; import com.facebook.drawee.backends.pipeline.Fresco; @@ -24,15 +24,13 @@ import com.facebook.drawee.controller.BaseControllerListener; import com.facebook.drawee.interfaces.DraweeController; import com.facebook.imagepipeline.image.ImageInfo; -import java.io.File; - -import awais.instagrabber.R; import awais.instagrabber.databinding.DialogProfilepicBinding; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.UserService; @@ -182,14 +180,18 @@ public class ProfilePicDialogFragment extends DialogFragment { private void downloadProfilePicture() { if (url == null) return; - final File dir = new File(Environment.getExternalStorageDirectory(), "Download"); + // final File dir = new File(Environment.getExternalStorageDirectory(), "Download"); final Context context = getContext(); if (context == null) return; - if (dir.exists() || dir.mkdirs()) { - final File saveFile = new File(dir, name + '_' + System.currentTimeMillis() + ".jpg"); - DownloadUtils.download(context, url, saveFile.getAbsolutePath()); - return; - } - Toast.makeText(context, R.string.downloader_error_creating_folder, Toast.LENGTH_SHORT).show(); + // if (dir.exists() || dir.mkdirs()) { + // + // } + final String fileName = name + '_' + System.currentTimeMillis() + ".jpg"; + // final File saveFile = new File(dir, fileName); + final DocumentFile downloadDir = DownloadUtils.getDownloadDir(); + final DocumentFile saveFile = downloadDir.createFile(Utils.mimeTypeMap.getMimeTypeFromExtension("jpg"), fileName); + DownloadUtils.download(context, url, saveFile); + // return; + // Toast.makeText(context, R.string.downloader_error_creating_folder, Toast.LENGTH_SHORT).show(); } } diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java index 285b4689..01cc0bd4 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java @@ -1,22 +1,43 @@ package awais.instagrabber.fragments.settings; import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatButton; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.documentfile.provider.DocumentFile; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; +import androidx.preference.PreferenceViewHolder; import androidx.preference.SwitchPreferenceCompat; +import com.google.android.material.switchmaterial.SwitchMaterial; + import awais.instagrabber.R; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.TextUtils; + +import static android.app.Activity.RESULT_OK; +import static awais.instagrabber.utils.Constants.FOLDER_PATH; +import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO; +import static awais.instagrabber.utils.Utils.settingsHelper; public class DownloadsPreferencesFragment extends BasePreferencesFragment { + private static final String TAG = DownloadsPreferencesFragment.class.getSimpleName(); + private static final int SELECT_DIR_REQUEST_CODE = 1; + private SaveToCustomFolderPreference.ResultCallback resultCallback; + @Override void setupPreferenceScreen(final PreferenceScreen screen) { final Context context = getContext(); if (context == null) return; screen.addPreference(getDownloadUserFolderPreference(context)); - // screen.addPreference(getSaveToCustomFolderPreference(context)); + screen.addPreference(getSaveToCustomFolderPreference(context)); } private Preference getDownloadUserFolderPreference(@NonNull final Context context) { @@ -27,63 +48,89 @@ public class DownloadsPreferencesFragment extends BasePreferencesFragment { return preference; } - // private Preference getSaveToCustomFolderPreference(@NonNull final Context context) { - // return new SaveToCustomFolderPreference(context, (resultCallback) -> new DirectoryChooser() - // .setInitialDirectory(settingsHelper.getString(FOLDER_PATH)) - // .setInteractionListener(file -> { - // settingsHelper.putString(FOLDER_PATH, file.getAbsolutePath()); - // resultCallback.onResult(file.getAbsolutePath()); - // }) - // .show(getParentFragmentManager(), null)); - // } - - // public static class SaveToCustomFolderPreference extends Preference { - // private AppCompatTextView customPathTextView; - // private final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener; - // private final String key; - // - // public SaveToCustomFolderPreference(final Context context, final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener) { - // super(context); - // this.onSelectFolderButtonClickListener = onSelectFolderButtonClickListener; - // key = Constants.FOLDER_SAVE_TO; - // setLayoutResource(R.layout.pref_custom_folder); - // setKey(key); - // setTitle(R.string.save_to_folder); - // setIconSpaceReserved(false); - // } - // - // @Override - // public void onBindViewHolder(final PreferenceViewHolder holder) { - // super.onBindViewHolder(holder); - // final SwitchMaterial cbSaveTo = (SwitchMaterial) holder.findViewById(R.id.cbSaveTo); - // final View buttonContainer = holder.findViewById(R.id.button_container); - // customPathTextView = (AppCompatTextView) holder.findViewById(R.id.custom_path); - // cbSaveTo.setOnCheckedChangeListener((buttonView, isChecked) -> { - // settingsHelper.putBoolean(FOLDER_SAVE_TO, isChecked); - // buttonContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE); - // final String customPath = settingsHelper.getString(FOLDER_PATH); - // customPathTextView.setText(customPath); - // }); - // final boolean savedToEnabled = settingsHelper.getBoolean(key); - // holder.itemView.setOnClickListener(v -> cbSaveTo.toggle()); - // cbSaveTo.setChecked(savedToEnabled); - // buttonContainer.setVisibility(savedToEnabled ? View.VISIBLE : View.GONE); - // final AppCompatButton btnSaveTo = (AppCompatButton) holder.findViewById(R.id.btnSaveTo); - // btnSaveTo.setOnClickListener(v -> { - // if (onSelectFolderButtonClickListener == null) return; - // onSelectFolderButtonClickListener.onClick(result -> { - // if (TextUtils.isEmpty(result)) return; - // customPathTextView.setText(result); - // }); - // }); - // } - // - // public interface ResultCallback { - // void onResult(String result); - // } - // - // public interface OnSelectFolderButtonClickListener { - // void onClick(ResultCallback resultCallback); - // } - // } + private Preference getSaveToCustomFolderPreference(@NonNull final Context context) { + return new SaveToCustomFolderPreference(context, (resultCallback) -> { + // Choose a directory using the system's file picker. + final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + startActivityForResult(intent, SELECT_DIR_REQUEST_CODE); + this.resultCallback = resultCallback; + + // new DirectoryChooser() + // .setInitialDirectory(settingsHelper.getString(FOLDER_PATH)) + // .setInteractionListener(file -> { + // settingsHelper.putString(FOLDER_PATH, file.getAbsolutePath()); + // resultCallback.onResult(file.getAbsolutePath()); + // }) + // .show(getParentFragmentManager(), null); + }); + } + + @Override + public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) { + if (data == null || data.getData() == null) return; + if (resultCode != RESULT_OK || requestCode != SELECT_DIR_REQUEST_CODE) return; + final Context context = getContext(); + if (context == null) return; + final Uri dirUri = data.getData(); + Log.d(TAG, "onActivityResult: " + dirUri); + final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + context.getContentResolver().takePersistableUriPermission(dirUri, takeFlags); + final DocumentFile root = DocumentFile.fromTreeUri(context, dirUri); + settingsHelper.putString(FOLDER_PATH, data.getData().toString()); + if (resultCallback != null) { + resultCallback.onResult(root.getName()); + resultCallback = null; + } + // Log.d(TAG, "onActivityResult: " + root); + } + + public static class SaveToCustomFolderPreference extends Preference { + private AppCompatTextView customPathTextView; + private final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener; + private final String key; + + public SaveToCustomFolderPreference(final Context context, final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener) { + super(context); + this.onSelectFolderButtonClickListener = onSelectFolderButtonClickListener; + key = FOLDER_SAVE_TO; + setLayoutResource(R.layout.pref_custom_folder); + setKey(key); + setTitle(R.string.save_to_folder); + setIconSpaceReserved(false); + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + final SwitchMaterial cbSaveTo = (SwitchMaterial) holder.findViewById(R.id.cbSaveTo); + final View buttonContainer = holder.findViewById(R.id.button_container); + customPathTextView = (AppCompatTextView) holder.findViewById(R.id.custom_path); + cbSaveTo.setOnCheckedChangeListener((buttonView, isChecked) -> { + settingsHelper.putBoolean(FOLDER_SAVE_TO, isChecked); + buttonContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE); + final String customPath = settingsHelper.getString(FOLDER_PATH); + customPathTextView.setText(customPath); + }); + final boolean savedToEnabled = settingsHelper.getBoolean(key); + holder.itemView.setOnClickListener(v -> cbSaveTo.toggle()); + cbSaveTo.setChecked(savedToEnabled); + buttonContainer.setVisibility(savedToEnabled ? View.VISIBLE : View.GONE); + final AppCompatButton btnSaveTo = (AppCompatButton) holder.findViewById(R.id.btnSaveTo); + btnSaveTo.setOnClickListener(v -> { + if (onSelectFolderButtonClickListener == null) return; + onSelectFolderButtonClickListener.onClick(result -> { + if (TextUtils.isEmpty(result)) return; + customPathTextView.setText(result); + }); + }); + } + + public interface ResultCallback { + void onResult(String result); + } + + public interface OnSelectFolderButtonClickListener { + void onClick(ResultCallback resultCallback); + } + } } diff --git a/app/src/main/java/awais/instagrabber/services/DeleteImageIntentService.java b/app/src/main/java/awais/instagrabber/services/DeleteImageIntentService.java index b9442527..ceeb2c7c 100644 --- a/app/src/main/java/awais/instagrabber/services/DeleteImageIntentService.java +++ b/app/src/main/java/awais/instagrabber/services/DeleteImageIntentService.java @@ -4,13 +4,14 @@ import android.app.IntentService; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationManagerCompat; +import androidx.documentfile.provider.DocumentFile; -import java.io.File; import java.util.Random; import awais.instagrabber.utils.TextUtils; @@ -39,7 +40,10 @@ public class DeleteImageIntentService extends IntentService { if (intent != null && Intent.ACTION_DELETE.equals(intent.getAction()) && intent.hasExtra(EXTRA_IMAGE_PATH)) { final String path = intent.getStringExtra(EXTRA_IMAGE_PATH); if (TextUtils.isEmpty(path)) return; - final File file = new File(path); + // final File file = new File(path); + final Uri parse = Uri.parse(path); + if (parse == null) return; + final DocumentFile file = DocumentFile.fromSingleUri(getApplicationContext(), parse); boolean deleted; if (file.exists()) { deleted = file.delete(); @@ -58,11 +62,11 @@ public class DeleteImageIntentService extends IntentService { @NonNull public static PendingIntent pendingIntent(@NonNull final Context context, - @NonNull final String imagePath, + @NonNull final DocumentFile imagePath, final int notificationId) { final Intent intent = new Intent(context, DeleteImageIntentService.class); intent.setAction(Intent.ACTION_DELETE); - intent.putExtra(EXTRA_IMAGE_PATH, imagePath); + intent.putExtra(EXTRA_IMAGE_PATH, imagePath.getUri().toString()); intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId); return PendingIntent.getService(context, random.nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT); } diff --git a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java index c0b4cb5f..01fef306 100644 --- a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.java @@ -11,14 +11,13 @@ import android.util.LruCache; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; +import androidx.documentfile.provider.DocumentFile; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import java.io.File; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -247,12 +246,14 @@ public final class BitmapUtils { return null; } - public static File convertToJpegAndSaveToFile(@NonNull final Bitmap bitmap, @Nullable final File file) throws IOException { - File tempFile = file; + public static DocumentFile convertToJpegAndSaveToFile(@NonNull final ContentResolver contentResolver, + @NonNull final Bitmap bitmap, + @Nullable final DocumentFile file) throws IOException { + DocumentFile tempFile = file; if (file == null) { - tempFile = DownloadUtils.getTempFile(); + tempFile = DownloadUtils.getTempFile(null, "jpg"); } - try (OutputStream output = new FileOutputStream(tempFile)) { + try (OutputStream output = contentResolver.openOutputStream(tempFile.getUri())) { final boolean compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output); if (!compressResult) { throw new RuntimeException("Compression failed!"); diff --git a/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java b/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java index a5933e48..6f54dfcb 100644 --- a/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java @@ -1,9 +1,11 @@ package awais.instagrabber.utils; import android.Manifest; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; -import android.os.Environment; +import android.net.Uri; +import android.provider.DocumentsContract; import android.util.Log; import android.webkit.MimeTypeMap; import android.widget.Toast; @@ -11,6 +13,8 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.core.util.Pair; +import androidx.documentfile.provider.DocumentFile; import androidx.work.Constraints; import androidx.work.Data; import androidx.work.NetworkType; @@ -21,9 +25,9 @@ import androidx.work.WorkRequest; import com.google.gson.Gson; import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -41,31 +45,50 @@ import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.VideoVersion; import awais.instagrabber.workers.DownloadWorker; +import static awais.instagrabber.utils.Constants.FOLDER_PATH; + public final class DownloadUtils { private static final String TAG = DownloadUtils.class.getSimpleName(); public static final String WRITE_PERMISSION = Manifest.permission.WRITE_EXTERNAL_STORAGE; public static final String[] PERMS = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; - public static final String DIR_BARINSTA = "Barinsta"; - public static final String DIR_DOWNLOADS = "Downloads"; - public static final String DIR_CAMERA = "Camera"; - public static final String DIR_EDIT = "Edit"; - - public static File getDownloadDir(final String... dirs) { - final File parent = new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS); - File subDir = new File(parent, DIR_BARINSTA); + private static final String DIR_BARINSTA = "Barinsta"; + private static final String DIR_DOWNLOADS = "Downloads"; + private static final String DIR_CAMERA = "Camera"; + private static final String DIR_EDIT = "Edit"; + private static final String TEMP_DIR = "Temp"; + + private static DocumentFile root; + + public static void init(@NonNull final Context context) { + // if (!Utils.settingsHelper.getBoolean(FOLDER_SAVE_TO)) return; + final String customPath = Utils.settingsHelper.getString(FOLDER_PATH); + if (TextUtils.isEmpty(customPath)) return; + // dir = new File(customPath); + root = DocumentFile.fromTreeUri(context, Uri.parse(customPath)); + Log.d(TAG, "init: " + root); + // final File parent = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + // final DocumentFile documentFile = DocumentFile.fromFile(parent); + // Log.d(TAG, "init: " + documentFile); + } + + public static DocumentFile getDownloadDir(final String... dirs) { + // final File parent = new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS); + // File subDir = new File(parent, DIR_BARINSTA); + DocumentFile subDir = root; if (dirs != null) { for (final String dir : dirs) { - subDir = new File(subDir, dir); - //noinspection ResultOfMethodCallIgnored - subDir.mkdirs(); + final DocumentFile subDirFile = subDir.findFile(dir); + if (subDirFile == null) { + subDir = subDir.createDirectory(dir); + } } } return subDir; } @NonNull - public static File getDownloadDir() { + public static DocumentFile getDownloadDir() { // final File parent = new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS); // final File dir = new File(new File(parent, "barinsta"), "downloads"); // if (!dir.exists()) { @@ -83,37 +106,63 @@ public final class DownloadUtils { return getDownloadDir(DIR_DOWNLOADS); } - public static File getCameraDir() { + public static DocumentFile getCameraDir() { return getDownloadDir(DIR_CAMERA); } - public static File getImageEditDir(final String sessionId) { + public static DocumentFile getImageEditDir(final String sessionId) { return getDownloadDir(DIR_EDIT, sessionId); } - @Nullable - private static File getDownloadDir(@NonNull final Context context, @Nullable final String username) { - return getDownloadDir(context, username, false); - } + // @Nullable + // private static DocumentFile getDownloadDir(@NonNull final Context context, @Nullable final String username) { + // return getDownloadDir(context, username, false); + // } @Nullable - private static File getDownloadDir(final Context context, - @Nullable final String username, - final boolean skipCreateDir) { - File dir = getDownloadDir(); - - if (Utils.settingsHelper.getBoolean(Constants.DOWNLOAD_USER_FOLDER) && !TextUtils.isEmpty(username)) { - final String finaleUsername = username.startsWith("@") ? username.substring(1) : username; - dir = new File(dir, finaleUsername); + private static DocumentFile getDownloadDir(final Context context, + @Nullable final String username) { + final List userFolderPaths = getSubPathForUserFolder(username); + DocumentFile dir = root; + for (final String dirName : userFolderPaths) { + final DocumentFile file = dir.findFile(dirName); + if (file != null) { + dir = file; + continue; + } + dir = dir.createDirectory(dirName); + if (dir == null) break; } - - if (context != null && !skipCreateDir && !dir.exists() && !dir.mkdirs()) { + // final String joined = android.text.TextUtils.join("/", userFolderPaths); + // final Uri userFolderUri = DocumentsContract.buildDocumentUriUsingTree(root.getUri(), joined); + // final DocumentFile userFolder = DocumentFile.fromSingleUri(context, userFolderUri); + if (context != null && (dir == null || !dir.exists())) { Toast.makeText(context, R.string.error_creating_folders, Toast.LENGTH_SHORT).show(); return null; } return dir; } + private static List getSubPathForUserFolder(final String username) { + final List list = new ArrayList<>(); + if (!Utils.settingsHelper.getBoolean(Constants.DOWNLOAD_USER_FOLDER) || TextUtils.isEmpty(username)) { + list.add(DIR_DOWNLOADS); + return list; + } + final String finalUsername = username.startsWith("@") ? username.substring(1) : username; + list.add(DIR_DOWNLOADS); + list.add(finalUsername); + return list; + } + + private static DocumentFile getTempDir() { + DocumentFile file = root.findFile(TEMP_DIR); + if (file == null) { + file = root.createDirectory(TEMP_DIR); + } + return file; + } + // public static void dmDownload(@NonNull final Context context, // @Nullable final String username, // final String modelId, @@ -126,59 +175,71 @@ public final class DownloadUtils { // } // } - private static void dmDownloadImpl(@NonNull final Context context, - @Nullable final String username, - final String modelId, - final String url) { - final File dir = getDownloadDir(context, username); - if (dir.exists() || dir.mkdirs()) { - download(context, - url, - getDownloadSaveFile(dir, modelId, url).getAbsolutePath()); - return; - } - Toast.makeText(context, R.string.error_creating_folders, Toast.LENGTH_SHORT).show(); - } + // private static void dmDownloadImpl(@NonNull final Context context, + // @Nullable final String username, + // final String modelId, + // final String url) { + // final DocumentFile dir = getDownloadDir(context, username); + // if (dir != null && dir.exists()) { + // download(context, url, getDownloadSavePaths(dir, modelId, url)); + // return; + // } + // Toast.makeText(context, R.string.error_creating_folders, Toast.LENGTH_SHORT).show(); + // } @NonNull - private static File getDownloadSaveFile(final File finalDir, - final String postId, - final String displayUrl) { - return getDownloadSaveFile(finalDir, postId, "", displayUrl); + private static Pair, String> getDownloadSavePaths(final List paths, + final String postId, + final String displayUrl) { + return getDownloadSavePaths(paths, postId, "", displayUrl); } - private static File getDownloadChildSaveFile(final File downloadDir, - final String postId, - final int childPosition, - final String url) { + private static Pair, String> getDownloadChildSaveFile(final List paths, + final String postId, + final int childPosition, + final String url) { final String sliderPostfix = "_slide_" + childPosition; - return getDownloadSaveFile(downloadDir, postId, sliderPostfix, url); + return getDownloadSavePaths(paths, postId, sliderPostfix, url); } - @NonNull - private static File getDownloadSaveFile(final File finalDir, - final String postId, - final String sliderPostfix, - final String displayUrl) { - final String fileName = postId + sliderPostfix + getFileExtensionFromUrl(displayUrl); - return new File(finalDir, fileName); + @Nullable + private static Pair, String> getDownloadSavePaths(final List paths, + final String postId, + final String sliderPostfix, + final String displayUrl) { + if (paths == null) return null; + final String extension = getFileExtensionFromUrl(displayUrl); + final String fileName = postId + sliderPostfix + extension; + // return new File(finalDir, fileName); + // DocumentFile file = finalDir.findFile(fileName); + // if (file == null) { + final String mimeType = Utils.mimeTypeMap.getMimeTypeFromExtension(extension.startsWith(".") ? extension.substring(1) : extension); + // file = finalDir.createFile(mimeType, fileName); + // } + paths.add(fileName); + return new Pair<>(paths, mimeType); } - @NonNull - public static File getTempFile() { + public static DocumentFile getTempFile() { return getTempFile(null, null); } - public static File getTempFile(final String fileName, final String extension) { - final File dir = getDownloadDir(); + public static DocumentFile getTempFile(final String fileName, final String extension) { + final DocumentFile dir = getTempDir(); String name = fileName; if (TextUtils.isEmpty(name)) { name = UUID.randomUUID().toString(); } + String mimeType = "application/octet-stream"; if (!TextUtils.isEmpty(extension)) { name += "." + extension; + mimeType = Utils.mimeTypeMap.getMimeTypeFromExtension(extension); + } + DocumentFile file = dir.findFile(name); + if (file == null) { + file = dir.createFile(mimeType, name); } - return new File(dir, name); + return file; } /** @@ -221,20 +282,21 @@ public final class DownloadUtils { return ""; } - public static List checkDownloaded(@NonNull final Media media) { + public static List checkDownloaded(@NonNull final Context context, + @NonNull final Media media) { final List checkList = new LinkedList<>(); final User user = media.getUser(); String username = "username"; if (user != null) { username = user.getUsername(); } - final File downloadDir = getDownloadDir(null, "@" + username, true); + final List userFolderPaths = getSubPathForUserFolder(username); switch (media.getMediaType()) { case MEDIA_TYPE_IMAGE: case MEDIA_TYPE_VIDEO: { final String url = ResponseBodyUtils.getImageUrl(media); - final File file = getDownloadSaveFile(downloadDir, media.getCode(), url); - checkList.add(file.exists()); + final Pair, String> pair = getDownloadSavePaths(userFolderPaths, media.getCode(), url); + checkList.add(checkPathExists(context, pair.first)); break; } case MEDIA_TYPE_SLIDER: @@ -243,8 +305,8 @@ public final class DownloadUtils { final Media child = sliderItems.get(i); if (child == null) continue; final String url = ResponseBodyUtils.getImageUrl(child); - final File file = getDownloadChildSaveFile(downloadDir, media.getCode(), i + 1, url); - checkList.add(file.exists()); + final Pair, String> pair = getDownloadChildSaveFile(userFolderPaths, media.getCode(), i + 1, url); + checkList.add(checkPathExists(context, pair.first)); } break; default: @@ -252,6 +314,14 @@ public final class DownloadUtils { return checkList; } + private static boolean checkPathExists(@NonNull final Context context, + @NonNull final List paths) { + final String joined = android.text.TextUtils.join("/", paths); + final Uri userFolderUri = DocumentsContract.buildDocumentUriUsingTree(root.getUri(), joined); + final DocumentFile userFolder = DocumentFile.fromSingleUri(context, userFolderUri); + return userFolder != null && userFolder.exists(); + } + public static void showDownloadDialog(@NonNull Context context, @NonNull final Media feedModel, final int childPosition) { @@ -286,15 +356,20 @@ public final class DownloadUtils { public static void download(@NonNull final Context context, @NonNull final StoryModel storyModel) { - final File downloadDir = getDownloadDir(context, "@" + storyModel.getUsername()); + final DocumentFile downloadDir = getDownloadDir(context, "@" + storyModel.getUsername()); final String url = storyModel.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO ? storyModel.getVideoUrl() : storyModel.getStoryUrl(); - final File saveFile = new File(downloadDir, - storyModel.getStoryMediaId() - + "_" + storyModel.getTimestamp() - + DownloadUtils.getFileExtensionFromUrl(url)); - download(context, url, saveFile.getAbsolutePath()); + final String extension = DownloadUtils.getFileExtensionFromUrl(url); + final String fileName = storyModel.getStoryMediaId() + "_" + storyModel.getTimestamp() + extension; + DocumentFile saveFile = downloadDir.findFile(fileName); + if (saveFile == null) { + saveFile = downloadDir.createFile( + Utils.mimeTypeMap.getMimeTypeFromExtension(extension.startsWith(".") ? extension.substring(1) : extension), + fileName); + } + // final File saveFile = new File(downloadDir, fileName); + download(context, url, saveFile); } public static void download(@NonNull final Context context, @@ -316,17 +391,19 @@ public final class DownloadUtils { private static void download(@NonNull final Context context, @NonNull final List feedModels, final int childPositionIfSingle) { - final Map map = new HashMap<>(); + final Map map = new HashMap<>(); for (final Media media : feedModels) { final User mediaUser = media.getUser(); - final File downloadDir = getDownloadDir(context, mediaUser == null ? "" : "@" + mediaUser.getUsername()); - if (downloadDir == null) return; + final List userFolderPaths = getSubPathForUserFolder(mediaUser == null ? "" : "@" + mediaUser.getUsername()); + // final DocumentFile downloadDir = getDownloadDir(context, mediaUser == null ? "" : "@" + mediaUser.getUsername()); switch (media.getMediaType()) { case MEDIA_TYPE_IMAGE: case MEDIA_TYPE_VIDEO: { final String url = getUrlOfType(media); - final File file = getDownloadSaveFile(downloadDir, media.getCode(), url); - map.put(url, file.getAbsolutePath()); + final Pair, String> pair = getDownloadSavePaths(userFolderPaths, media.getCode(), url); + final DocumentFile file = createFile(pair); + if (file == null) continue; + map.put(url, file); break; } case MEDIA_TYPE_VOICE: { @@ -335,28 +412,48 @@ public final class DownloadUtils { if (mediaUser != null) { fileName = mediaUser.getUsername() + "_" + fileName; } - final File file = getDownloadSaveFile(downloadDir, fileName, url); - map.put(url, file.getAbsolutePath()); + final Pair, String> pair = getDownloadSavePaths(userFolderPaths, fileName, url); + final DocumentFile file = createFile(pair); + if (file == null) continue; + map.put(url, file); break; } case MEDIA_TYPE_SLIDER: final List sliderItems = media.getCarouselMedia(); for (int i = 0; i < sliderItems.size(); i++) { - if (childPositionIfSingle >= 0 && feedModels.size() == 1 && i != childPositionIfSingle) { - continue; - } + if (childPositionIfSingle >= 0 && feedModels.size() == 1 && i != childPositionIfSingle) continue; final Media child = sliderItems.get(i); final String url = getUrlOfType(child); - final File file = getDownloadChildSaveFile(downloadDir, media.getCode(), i + 1, url); - map.put(url, file.getAbsolutePath()); + final Pair, String> pair = getDownloadChildSaveFile(userFolderPaths, media.getCode(), i + 1, url); + final DocumentFile file = createFile(pair); + if (file == null) continue; + map.put(url, file); } break; default: } } + if (map.isEmpty()) return; download(context, map); } + private static DocumentFile createFile(@NonNull final Pair, String> pair) { + if (pair.first == null || pair.second == null) return null; + DocumentFile dir = root; + final List first = pair.first; + for (int i = 0; i < first.size(); i++) { + final String name = first.get(i); + final DocumentFile file = dir.findFile(name); + if (file != null) { + dir = file; + continue; + } + dir = i == first.size() - 1 ? dir.createFile(pair.second, name) : dir.createDirectory(name); + if (dir == null) break; + } + return dir; + } + @Nullable private static String getUrlOfType(@NonNull final Media media) { switch (media.getMediaType()) { @@ -388,12 +485,13 @@ public final class DownloadUtils { public static void download(final Context context, final String url, - final String filePath) { + final DocumentFile filePath) { if (context == null || url == null || filePath == null) return; download(context, Collections.singletonMap(url, filePath)); } - private static void download(final Context context, final Map urlFilePathMap) { + private static void download(final Context context, final Map urlFilePathMap) { + if (context == null) return; final Constraints constraints = new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build(); @@ -401,19 +499,25 @@ public final class DownloadUtils { .setUrlToFilePathMap(urlFilePathMap) .build(); final String requestJson = new Gson().toJson(request); - final File tempFile = getTempFile(); - try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) { + final DocumentFile tempFile = getTempFile(null, "json"); + if (tempFile == null) { + Log.e(TAG, "download: temp file is null"); + return; + } + final Uri uri = tempFile.getUri(); + final ContentResolver contentResolver = context.getContentResolver(); + if (contentResolver == null) return; + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(contentResolver.openOutputStream(uri)))) { writer.write(requestJson); } catch (IOException e) { Log.e(TAG, "download: Error writing request to file", e); - //noinspection ResultOfMethodCallIgnored tempFile.delete(); return; } final WorkRequest downloadWorkRequest = new OneTimeWorkRequest.Builder(DownloadWorker.class) .setInputData( new Data.Builder() - .putString(DownloadWorker.KEY_DOWNLOAD_REQUEST_JSON, tempFile.getAbsolutePath()) + .putString(DownloadWorker.KEY_DOWNLOAD_REQUEST_JSON, tempFile.getUri().toString()) .build() ) .setConstraints(constraints) diff --git a/app/src/main/java/awais/instagrabber/utils/MediaUploader.java b/app/src/main/java/awais/instagrabber/utils/MediaUploader.java index f9c47a91..30570663 100644 --- a/app/src/main/java/awais/instagrabber/utils/MediaUploader.java +++ b/app/src/main/java/awais/instagrabber/utils/MediaUploader.java @@ -6,11 +6,10 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; import org.json.JSONObject; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Map; @@ -45,7 +44,7 @@ public final class MediaUploader { listener.onFailure(new RuntimeException("Bitmap result was null")); return; } - uploadPhoto(bitmap, listener); + uploadPhoto(contentResolver, bitmap, listener); } @Override @@ -55,13 +54,14 @@ public final class MediaUploader { }); } - private static void uploadPhoto(@NonNull final Bitmap bitmap, + private static void uploadPhoto(@NonNull final ContentResolver contentResolver, + @NonNull final Bitmap bitmap, @NonNull final OnMediaUploadCompleteListener listener) { appExecutors.tasksThread().submit(() -> { - final File file; + final DocumentFile file; final long byteLength; try { - file = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null); + file = BitmapUtils.convertToJpegAndSaveToFile(contentResolver, bitmap, null); byteLength = file.length(); } catch (Exception e) { listener.onFailure(e); @@ -71,12 +71,11 @@ public final class MediaUploader { final Map headers = MediaUploadHelper.getUploadPhotoHeaders(options); final String url = HOST + "/rupload_igphoto/" + options.getName() + "/"; appExecutors.networkIO().execute(() -> { - try (FileInputStream input = new FileInputStream(file)) { + try (InputStream input = contentResolver.openInputStream(file.getUri())) { upload(input, url, headers, listener); } catch (IOException e) { listener.onFailure(e); } finally { - //noinspection ResultOfMethodCallIgnored file.delete(); } }); diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java index 5d5c4bca..789133b8 100644 --- a/app/src/main/java/awais/instagrabber/utils/Utils.java +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -15,8 +15,11 @@ import android.media.MediaScannerConnection; import android.media.MediaScannerConnection.OnScanCompletedListener; import android.net.Uri; import android.os.Build; +import android.os.Environment; import android.os.Handler; +import android.os.storage.StorageManager; import android.provider.Browser; +import android.provider.DocumentsContract; import android.util.DisplayMetrics; import android.util.Log; import android.util.Pair; @@ -35,6 +38,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; +import androidx.documentfile.provider.DocumentFile; import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; import com.google.android.exoplayer2.database.ExoDatabaseProvider; @@ -46,6 +50,8 @@ import org.json.JSONObject; import java.io.File; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Map; @@ -71,6 +77,7 @@ public final class Utils { public static Handler applicationHandler; public static String cacheDir; private static int defaultStatusBarColor; + private static Object[] volumes; public static int convertDpToPx(final float dp) { return Math.round((dp * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT); @@ -367,4 +374,51 @@ public final class Utils { if (window == null) return; window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } + + public static void scanDocumentFile(@NonNull final Context context, + @NonNull final DocumentFile documentFile, + @NonNull final OnScanCompletedListener callback) { + if (!documentFile.isFile()) return; + File file = null; + try { + file = getDocumentFileRealPath(context, documentFile); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + Log.e(TAG, "scanDocumentFile: ", e); + } + if (file == null) return; + MediaScannerConnection.scanFile(context, + new String[]{file.getAbsolutePath()}, + new String[]{documentFile.getType()}, + callback); + } + + private static File getDocumentFileRealPath(Context context, DocumentFile documentFile) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + final String docId = DocumentsContract.getDocumentId(documentFile.getUri()); + final String[] split = docId.split(":"); + final String type = split[0]; + + if (type.equalsIgnoreCase("primary")) { + return new File(Environment.getExternalStorageDirectory(), split[1]); + } else { + if (volumes == null) { + StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + Method getVolumeListMethod = sm.getClass().getMethod("getVolumeList"); + volumes = (Object[]) getVolumeListMethod.invoke(sm); + } + + for (Object volume : volumes) { + Method getUuidMethod = volume.getClass().getMethod("getUuid"); + String uuid = (String) getUuidMethod.invoke(volume); + + if (uuid != null && uuid.equalsIgnoreCase(type)) { + Method getPathMethod = volume.getClass().getMethod("getPath"); + String path = (String) getPathMethod.invoke(volume); + return new File(path, split[1]); + } + } + } + + return null; + } } diff --git a/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java b/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java index a22f5055..d264226e 100644 --- a/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java +++ b/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java @@ -1,14 +1,16 @@ package awais.instagrabber.utils; +import android.app.Application; import android.media.MediaRecorder; import android.os.Handler; import android.os.Message; +import android.os.ParcelFileDescriptor; import android.util.Log; import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; +import androidx.documentfile.provider.DocumentFile; -import java.io.File; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -27,20 +29,20 @@ public class VoiceRecorder { private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat(FILE_FORMAT, Locale.US); private final List waveform = new ArrayList<>(); - private final File recordingsDir; + private final DocumentFile recordingsDir; private final VoiceRecorderCallback callback; private MediaRecorder recorder; - private File audioTempFile; + private DocumentFile audioTempFile; private MaxAmpHandler maxAmpHandler; private boolean stopped; - public VoiceRecorder(@NonNull final File recordingsDir, final VoiceRecorderCallback callback) { + public VoiceRecorder(@NonNull final DocumentFile recordingsDir, final VoiceRecorderCallback callback) { this.recordingsDir = recordingsDir; this.callback = callback; } - public void startRecording() { + public void startRecording(final Application application) { stopped = false; try { recorder = new MediaRecorder(); @@ -48,7 +50,8 @@ public class VoiceRecorder { recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); deleteTempAudioFile(); audioTempFile = getAudioRecordFile(); - recorder.setOutputFile(audioTempFile.getAbsolutePath()); + final ParcelFileDescriptor parcelFileDescriptor = application.getContentResolver().openFileDescriptor(audioTempFile.getUri(), "rwt"); + recorder.setOutputFile(parcelFileDescriptor.getFileDescriptor()); recorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC); recorder.setAudioEncodingBitRate(AUDIO_BIT_RATE); recorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE); @@ -140,9 +143,9 @@ public class VoiceRecorder { // } @NonNull - private File getAudioRecordFile() { + private DocumentFile getAudioRecordFile() { final String name = String.format("%s-%s.%s", FILE_PREFIX, SIMPLE_DATE_FORMAT.format(new Date()), EXTENSION); - return new File(recordingsDir, name); + return recordingsDir.createFile(MIME_TYPE, name); } private void deleteTempAudioFile() { @@ -160,11 +163,11 @@ public class VoiceRecorder { public static class VoiceRecordingResult { private final String mimeType; - private final File file; + private final DocumentFile file; private final List waveform; private final int samplingFreq = 10; - public VoiceRecordingResult(final String mimeType, final File file, final List waveform) { + public VoiceRecordingResult(final String mimeType, final DocumentFile file, final List waveform) { this.mimeType = mimeType; this.file = file; this.waveform = waveform; @@ -174,7 +177,7 @@ public class VoiceRecorder { return mimeType; } - public File getFile() { + public DocumentFile getFile() { return file; } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java index 3ad563c1..440d4765 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java @@ -2,18 +2,17 @@ package awais.instagrabber.viewmodels; import android.app.Application; import android.content.ContentResolver; -import android.media.MediaScannerConnection; import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; -import java.io.File; import java.util.List; import java.util.Map; import java.util.Objects; @@ -38,6 +37,7 @@ import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.MediaController; import awais.instagrabber.utils.MediaUtils; import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.VoiceRecorder; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -47,7 +47,7 @@ public class DirectThreadViewModel extends AndroidViewModel { // private static final String ERROR_INVALID_THREAD = "Invalid thread"; private final ContentResolver contentResolver; - private final File recordingsDir; + private final DocumentFile recordingsDir; private final Application application; private final long viewerId; private final String threadId; @@ -166,37 +166,32 @@ public class DirectThreadViewModel extends AndroidViewModel { @Override public void onComplete(final VoiceRecorder.VoiceRecordingResult result) { Log.d(TAG, "onComplete: recording complete. Scanning file..."); - MediaScannerConnection.scanFile( - application, - new String[]{result.getFile().getAbsolutePath()}, - new String[]{result.getMimeType()}, - (path, uri) -> { - if (uri == null) { - final String msg = "Scan failed!"; - Log.e(TAG, msg); - data.postValue(Resource.error(msg, null)); - return; - } - Log.d(TAG, "onComplete: scan complete"); - MediaUtils.getVoiceInfo(contentResolver, uri, new MediaUtils.OnInfoLoadListener() { - @Override - public void onLoad(@Nullable final MediaUtils.VideoInfo videoInfo) { - if (videoInfo == null) return; - threadManager.sendVoice(data, - uri, - result.getWaveform(), - result.getSamplingFreq(), - videoInfo == null ? 0 : videoInfo.duration, - videoInfo == null ? 0 : videoInfo.size); - } - - @Override - public void onFailure(final Throwable t) { - data.postValue(Resource.error(t.getMessage(), null)); - } - }); + Utils.scanDocumentFile(application, result.getFile(), (path, uri) -> { + if (uri == null) { + final String msg = "Scan failed!"; + Log.e(TAG, msg); + data.postValue(Resource.error(msg, null)); + return; + } + Log.d(TAG, "onComplete: scan complete"); + MediaUtils.getVoiceInfo(contentResolver, uri, new MediaUtils.OnInfoLoadListener() { + @Override + public void onLoad(@Nullable final MediaUtils.VideoInfo videoInfo) { + if (videoInfo == null) return; + threadManager.sendVoice(data, + uri, + result.getWaveform(), + result.getSamplingFreq(), + videoInfo == null ? 0 : videoInfo.duration, + videoInfo == null ? 0 : videoInfo.size); } - ); + + @Override + public void onFailure(final Throwable t) { + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + }); } @Override @@ -204,7 +199,7 @@ public class DirectThreadViewModel extends AndroidViewModel { } }); - voiceRecorder.startRecording(); + voiceRecorder.startRecording(application); return data; } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/ImageEditViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/ImageEditViewModel.java index ecd12260..7a8613ea 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/ImageEditViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/ImageEditViewModel.java @@ -5,6 +5,7 @@ import android.graphics.RectF; import android.net.Uri; import androidx.annotation.NonNull; +import androidx.documentfile.provider.DocumentFile; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -26,6 +27,7 @@ import awais.instagrabber.models.SavedImageEditState; import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.SerializablePair; +import awais.instagrabber.utils.Utils; import jp.co.cyberagent.android.gpuimage.GPUImage; import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter; import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilterGroup; @@ -35,6 +37,7 @@ public class ImageEditViewModel extends AndroidViewModel { private static final String RESULT = "result"; private static final String FILE_FORMAT = "yyyyMMddHHmmssSSS"; private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat(FILE_FORMAT, Locale.US); + private static final String MIME_TYPE = Utils.mimeTypeMap.getMimeTypeFromExtension("jpg"); private Uri originalUri; private SavedImageEditState savedImageEditState; @@ -48,18 +51,18 @@ public class ImageEditViewModel extends AndroidViewModel { private final MutableLiveData isCropped = new MutableLiveData<>(false); private final MutableLiveData isTuned = new MutableLiveData<>(false); private final MutableLiveData isFiltered = new MutableLiveData<>(false); - private final File outputDir; + private final DocumentFile outputDir; private List> tuningFilters; private Filter appliedFilter; - private final File destinationFile; + private final DocumentFile destinationFile; public ImageEditViewModel(final Application application) { super(application); sessionId = SIMPLE_DATE_FORMAT.format(new Date()); outputDir = DownloadUtils.getImageEditDir(sessionId); - destinationFile = new File(outputDir, RESULT + ".jpg"); - destinationUri = Uri.fromFile(destinationFile); - cropDestinationUri = Uri.fromFile(new File(outputDir, CROP + ".jpg")); + destinationFile = outputDir.createFile(MIME_TYPE, RESULT + ".jpg"); + destinationUri = destinationFile.getUri(); + cropDestinationUri = outputDir.createFile(MIME_TYPE, CROP + ".jpg").getUri(); } public String getSessionId() { @@ -159,16 +162,15 @@ public class ImageEditViewModel extends AndroidViewModel { delete(outputDir); } - private void delete(@NonNull final File file) { + private void delete(@NonNull final DocumentFile file) { if (file.isDirectory()) { - final File[] files = file.listFiles(); + final DocumentFile[] files = file.listFiles(); if (files != null) { - for (File f : files) { + for (DocumentFile f : files) { delete(f); } } } - //noinspection ResultOfMethodCallIgnored file.delete(); } @@ -206,9 +208,9 @@ public class ImageEditViewModel extends AndroidViewModel { return new SerializablePair<>(type, propertyValueMap); } - public File getDestinationFile() { - return destinationFile; - } + // public File getDestinationFile() { + // return destinationFile; + // } public enum Tab { RESULT, diff --git a/app/src/main/java/awais/instagrabber/workers/DownloadWorker.java b/app/src/main/java/awais/instagrabber/workers/DownloadWorker.java index b0dae625..12669ed0 100644 --- a/app/src/main/java/awais/instagrabber/workers/DownloadWorker.java +++ b/app/src/main/java/awais/instagrabber/workers/DownloadWorker.java @@ -8,7 +8,6 @@ import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.MediaMetadataRetriever; -import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Build; import android.os.Handler; @@ -18,7 +17,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; -import androidx.core.content.FileProvider; +import androidx.documentfile.provider.DocumentFile; import androidx.work.Data; import androidx.work.ForegroundInfo; import androidx.work.Worker; @@ -30,10 +29,8 @@ import com.google.gson.JsonSyntaxException; import org.apache.commons.imaging.formats.jpeg.iptc.JpegIptcRewriter; import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.InputStream; +import java.io.OutputStream; import java.net.URL; import java.net.URLConnection; import java.util.Collection; @@ -44,6 +41,7 @@ import java.util.Map; import java.util.Scanner; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; import awais.instagrabber.BuildConfig; import awais.instagrabber.R; @@ -52,10 +50,11 @@ import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; -//import awaisomereport.LogCollector; import static awais.instagrabber.utils.Constants.DOWNLOAD_CHANNEL_ID; import static awais.instagrabber.utils.Constants.NOTIF_GROUP_NAME; + +//import awaisomereport.LogCollector; //import static awais.instagrabber.utils.Utils.logCollector; public class DownloadWorker extends Worker { @@ -85,8 +84,21 @@ public class DownloadWorker extends Worker { .build()); } final String downloadRequestString; - final File requestFile = new File(downloadRequestFilePath); - try (Scanner scanner = new Scanner(requestFile)) { + // final File requestFile = new File(downloadRequestFilePath); + final Uri requestFile = Uri.parse(downloadRequestFilePath); + if (requestFile == null) { + return Result.failure(new Data.Builder() + .putString("error", "requestFile is null") + .build()); + } + final Context context = getApplicationContext(); + final ContentResolver contentResolver = context.getContentResolver(); + if (contentResolver == null) { + return Result.failure(new Data.Builder() + .putString("error", "contentResolver is null") + .build()); + } + try (Scanner scanner = new Scanner(contentResolver.openInputStream(requestFile))) { downloadRequestString = scanner.useDelimiter("\\A").next(); } catch (Exception e) { Log.e(TAG, "doWork: ", e); @@ -116,7 +128,7 @@ public class DownloadWorker extends Worker { final Map urlToFilePathMap = downloadRequest.getUrlToFilePathMap(); download(urlToFilePathMap); new Handler(Looper.getMainLooper()).postDelayed(() -> showSummary(urlToFilePathMap), 500); - final boolean deleted = requestFile.delete(); + final boolean deleted = DocumentFile.fromSingleUri(context, requestFile).delete(); if (!deleted) { Log.w(TAG, "doWork: requestFile not deleted!"); } @@ -131,7 +143,9 @@ public class DownloadWorker extends Worker { for (final Map.Entry urlAndFilePath : entries) { final String url = urlAndFilePath.getKey(); updateDownloadProgress(notificationId, count, total, 0); - download(notificationId, count, total, url, urlAndFilePath.getValue()); + final String uriString = urlAndFilePath.getValue(); + final DocumentFile file = DocumentFile.fromSingleUri(getApplicationContext(), Uri.parse(uriString)); + download(notificationId, count, total, url, file); count++; } } @@ -144,17 +158,24 @@ public class DownloadWorker extends Worker { final int position, final int total, final String url, - final String filePath) { - final boolean isJpg = filePath.endsWith("jpg"); + final DocumentFile filePath) { + final Context context = getApplicationContext(); + if (context == null) return; + final ContentResolver contentResolver = context.getContentResolver(); + if (contentResolver == null) return; + final String filePathType = filePath.getType(); + if (filePathType == null) return; + // final String extension = Utils.mimeTypeMap.getExtensionFromMimeType(filePathType); + final boolean isJpg = filePathType.startsWith("image"); // extension.endsWith("jpg"); // using temp file approach to remove IPTC so that download progress can be reported - final File outFile = isJpg ? DownloadUtils.getTempFile() : new File(filePath); + final DocumentFile outFile = isJpg ? DownloadUtils.getTempFile(null, "jpg") : filePath; try { final URLConnection urlConnection = new URL(url).openConnection(); final long fileSize = Build.VERSION.SDK_INT >= 24 ? urlConnection.getContentLengthLong() : urlConnection.getContentLength(); float totalRead = 0; try (final BufferedInputStream bis = new BufferedInputStream(urlConnection.getInputStream()); - final FileOutputStream fos = new FileOutputStream(outFile)) { + final OutputStream fos = contentResolver.openOutputStream(outFile.getUri())) { final byte[] buffer = new byte[0x2000]; int count; while ((count = bis.read(buffer, 0, 0x2000)) != -1) { @@ -167,18 +188,17 @@ public class DownloadWorker extends Worker { } fos.flush(); } catch (final Exception e) { - Log.e(TAG, "Error while writing data from url: " + url + " to file: " + outFile.getAbsolutePath(), e); + Log.e(TAG, "Error while writing data from url: " + url + " to file: " + outFile.getName(), e); } if (isJpg) { - final File finalFile = new File(filePath); - try (FileInputStream fis = new FileInputStream(outFile); - FileOutputStream fos = new FileOutputStream(finalFile)) { + try (final InputStream fis = contentResolver.openInputStream(outFile.getUri()); + final OutputStream fos = contentResolver.openOutputStream(filePath.getUri())) { final JpegIptcRewriter jpegIptcRewriter = new JpegIptcRewriter(); jpegIptcRewriter.removeIPTC(fis, fos); } catch (Exception e) { Log.e(TAG, "Error while removing iptc: url: " + url - + ", tempFile: " + outFile.getAbsolutePath() - + ", finalFile: " + finalFile.getAbsolutePath(), e); + + ", tempFile: " + outFile + + ", finalFile: " + filePath, e); } final boolean deleted = outFile.delete(); if (!deleted) { @@ -243,57 +263,56 @@ public class DownloadWorker extends Worker { private void showSummary(final Map urlToFilePathMap) { final Context context = getApplicationContext(); - final Collection filePaths = urlToFilePathMap.values(); + final Collection filePaths = urlToFilePathMap.values() + .stream() + .map(s -> DocumentFile.fromSingleUri(context, Uri.parse(s))) + .collect(Collectors.toList()); final List notifications = new LinkedList<>(); final List notificationIds = new LinkedList<>(); int count = 1; - for (final String filePath : filePaths) { - final File file = new File(filePath); - context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); - MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null); - final Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file); + for (final DocumentFile filePath : filePaths) { + // final File file = new File(filePath); + context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, filePath.getUri())); + Utils.scanDocumentFile(context, filePath, (path, uri) -> {}); + // final Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file); final ContentResolver contentResolver = context.getContentResolver(); Bitmap bitmap = null; - final String mimeType = Utils.getMimeType(uri, contentResolver); + final String mimeType = filePath.getType(); // Utils.getMimeType(uri, contentResolver); if (!TextUtils.isEmpty(mimeType)) { if (mimeType.startsWith("image")) { - try (final InputStream inputStream = contentResolver.openInputStream(uri)) { + try (final InputStream inputStream = contentResolver.openInputStream(filePath.getUri())) { bitmap = BitmapFactory.decodeStream(inputStream); } catch (final Exception e) { -// if (logCollector != null) -// logCollector.appendException(e, LogCollector.LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_1"); if (BuildConfig.DEBUG) Log.e(TAG, "", e); } } else if (mimeType.startsWith("video")) { final MediaMetadataRetriever retriever = new MediaMetadataRetriever(); try { try { - retriever.setDataSource(context, uri); + retriever.setDataSource(context, filePath.getUri()); } catch (final Exception e) { - retriever.setDataSource(file.getAbsolutePath()); + // retriever.setDataSource(file.getAbsolutePath()); + Log.e(TAG, "showSummary: ", e); } bitmap = retriever.getFrameAtTime(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) try { retriever.close(); } catch (final Exception e) { -// if (logCollector != null) -// logCollector.appendException(e, LogCollector.LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_2"); + Log.e(TAG, "showSummary: ", e); } } catch (final Exception e) { - if (BuildConfig.DEBUG) Log.e(TAG, "", e); -// if (logCollector != null) -// logCollector.appendException(e, LogCollector.LogFile.ASYNC_DOWNLOADER, "onPostExecute::bitmap_3"); + Log.e(TAG, "", e); } } } final String downloadComplete = context.getString(R.string.downloader_complete); - final Intent intent = new Intent(Intent.ACTION_VIEW, uri) + final Intent intent = new Intent(Intent.ACTION_VIEW, filePath.getUri()) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_FROM_BACKGROUND | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - .putExtra(Intent.EXTRA_STREAM, uri); + .putExtra(Intent.EXTRA_STREAM, filePath.getUri()); final PendingIntent pendingIntent = PendingIntent.getActivity( context, DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE, @@ -357,16 +376,22 @@ public class DownloadWorker extends Worker { public static class Builder { private Map urlToFilePathMap; - public Builder setUrlToFilePathMap(final Map urlToFilePathMap) { - this.urlToFilePathMap = urlToFilePathMap; + public Builder setUrlToFilePathMap(final Map urlToFilePathMap) { + if (urlToFilePathMap == null) { + return this; + } + this.urlToFilePathMap = urlToFilePathMap.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, + o -> o.getValue().getUri().toString())); return this; } - public Builder addUrl(@NonNull final String url, @NonNull final String filePath) { + public Builder addUrl(@NonNull final String url, @NonNull final DocumentFile filePath) { if (urlToFilePathMap == null) { urlToFilePathMap = new HashMap<>(); } - urlToFilePathMap.put(url, filePath); + urlToFilePathMap.put(url, filePath.getUri().toString()); return this; }