diff --git a/app/build.gradle b/app/build.gradle index 3c520f75..b85aecad 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,7 +20,7 @@ android { applicationId 'me.austinhuang.instagrabber' minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 63 versionName '19.2.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bc617ff1..4e6d6d3f 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ package="awais.instagrabber"> - + @@ -20,7 +20,6 @@ android:fullBackupContent="@xml/backup_descriptor" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:requestLegacyExternalStorage="true" android:supportsRtl="true" android:theme="@style/AppTheme.Launcher" tools:ignore="UnusedAttribute"> @@ -98,6 +97,7 @@ + - Log.d(TAG, "onImageSaved: scan complete") - val intent = Intent() - intent.data = uri1 - setResult(RESULT_OK, intent) - finish() - } - Log.d(TAG, "onImageSaved: $uri") + try { outputStream.close() } catch (ignored: IOException) {} + val intent = Intent() + intent.data = photoFile.uri + setResult(RESULT_OK, intent) + finish() + Log.d(TAG, "onImageSaved: " + photoFile.uri) } override fun onError(exception: ImageCaptureException) { Log.e(TAG, "onError: ", exception) + try { outputStream.close() } catch (ignored: IOException) {} } } ) diff --git a/app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.java b/app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.java new file mode 100644 index 00000000..7fffbfec --- /dev/null +++ b/app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.java @@ -0,0 +1,115 @@ +package awais.instagrabber.activities; + +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.DocumentsContract; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +import awais.instagrabber.R; +import awais.instagrabber.databinding.ActivityDirectorySelectBinding; +import awais.instagrabber.dialogs.ConfirmDialogFragment; +import awais.instagrabber.utils.AppExecutors; +import awais.instagrabber.viewmodels.DirectorySelectActivityViewModel; + +public class DirectorySelectActivity extends BaseLanguageActivity { + private static final String TAG = DirectorySelectActivity.class.getSimpleName(); + public static final int SELECT_DIR_REQUEST_CODE = 0x01; + private static final int ERROR_REQUEST_CODE = 0x02; + + private Uri initialUri; + private ActivityDirectorySelectBinding binding; + private DirectorySelectActivityViewModel viewModel; + + @Override + protected void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityDirectorySelectBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + viewModel = new ViewModelProvider(this).get(DirectorySelectActivityViewModel.class); + setupObservers(); + binding.selectDir.setOnClickListener(v -> openDirectoryChooser()); + AppExecutors.INSTANCE.getMainThread().execute(() -> viewModel.setInitialUri(getIntent())); + } + + private void setupObservers() { + viewModel.getMessage().observe(this, message -> binding.message.setText(message)); + viewModel.getPrevUri().observe(this, prevUri -> { + if (prevUri == null) { + binding.prevUri.setVisibility(View.GONE); + binding.message2.setVisibility(View.GONE); + return; + } + binding.prevUri.setText(prevUri); + binding.prevUri.setVisibility(View.VISIBLE); + binding.message2.setVisibility(View.VISIBLE); + }); + viewModel.getDirSuccess().observe(this, success -> binding.selectDir.setVisibility(success ? View.GONE : View.VISIBLE)); + viewModel.isLoading().observe(this, loading -> { + binding.message.setVisibility(loading ? View.GONE : View.VISIBLE); + binding.loadingIndicator.setVisibility(loading ? View.VISIBLE : View.GONE); + }); + } + + private void openDirectoryChooser() { + final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialUri != null) { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri); + } + startActivityForResult(intent, SELECT_DIR_REQUEST_CODE); + } + + @Override + protected void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode != SELECT_DIR_REQUEST_CODE) return; + if (resultCode != RESULT_OK) { + showErrorDialog(getString(R.string.select_a_folder)); + return; + } + if (data == null || data.getData() == null) { + showErrorDialog(getString(R.string.select_a_folder)); + return; + } + AppExecutors.INSTANCE.getMainThread().execute(() -> { + try { + viewModel.setupSelectedDir(data); + final Intent intent = new Intent(this, MainActivity.class); + startActivity(intent); + finish(); + } catch (Exception e) { + // Should not come to this point. + // If it does, we have to show this error to the user so that they can report it. + try (final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw)) { + e.printStackTrace(pw); + showErrorDialog("Please report this error to the developers:\n\n" + sw.toString()); + } catch (IOException ioException) { + Log.e(TAG, "onActivityResult: ", ioException); + } + } + }, 500); + } + + private void showErrorDialog(@NonNull final String message) { + final ConfirmDialogFragment dialogFragment = ConfirmDialogFragment.newInstance( + ERROR_REQUEST_CODE, + R.string.error, + message, + R.string.ok, + 0, + 0 + ); + dialogFragment.show(getSupportFragmentManager(), ConfirmDialogFragment.class.getSimpleName()); + } +} diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.kt b/app/src/main/java/awais/instagrabber/activities/MainActivity.kt index 90be552a..adb1e728 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.kt +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.kt @@ -8,6 +8,7 @@ import android.content.ComponentName import android.content.Intent import android.content.ServiceConnection import android.os.* +import android.provider.DocumentsContract.EXTRA_INITIAL_URI import android.text.Editable import android.util.Log import android.view.Menu @@ -53,6 +54,7 @@ import awais.instagrabber.services.ActivityCheckerService import awais.instagrabber.services.DMSyncAlarmReceiver import awais.instagrabber.utils.* import awais.instagrabber.utils.AppExecutors.tasksThread +import awais.instagrabber.utils.DownloadUtils.ReselectDocumentTreeException import awais.instagrabber.utils.TextUtils.isEmpty import awais.instagrabber.utils.TextUtils.shortcodeToId import awais.instagrabber.utils.emoji.EmojiParser @@ -73,6 +75,7 @@ import kotlinx.coroutines.withContext import java.util.* import java.util.stream.Collectors + class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedListener { private lateinit var binding: ActivityMainBinding @@ -107,6 +110,16 @@ class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedL private val graphQLRepository: GraphQLRepository by lazy { GraphQLRepository.getInstance() } override fun onCreate(savedInstanceState: Bundle?) { + try { + DownloadUtils.init(this) + } catch (e: ReselectDocumentTreeException) { + super.onCreate(savedInstanceState) + val intent = Intent(this, DirectorySelectActivity::class.java) + intent.putExtra(EXTRA_INITIAL_URI, e.initialUri) + startActivity(intent) + finish() + return + } super.onCreate(savedInstanceState) instance = this binding = ActivityMainBinding.inflate(layoutInflater) diff --git a/app/src/main/java/awais/instagrabber/dialogs/ConfirmDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/ConfirmDialogFragment.java index f5b5bc81..70d4c9f3 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/ConfirmDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/ConfirmDialogFragment.java @@ -11,6 +11,7 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -73,6 +74,7 @@ public class ConfirmDialogFragment extends DialogFragment { ConfirmDialogFragment fragment = new ConfirmDialogFragment(); fragment.setArguments(args); return fragment; + } public ConfirmDialogFragment() {} @@ -80,11 +82,16 @@ public class ConfirmDialogFragment extends DialogFragment { @Override public void onAttach(@NonNull final Context context) { super.onAttach(context); + this.context = context; final Fragment parentFragment = getParentFragment(); if (parentFragment instanceof ConfirmDialogFragmentCallback) { callback = (ConfirmDialogFragmentCallback) parentFragment; + return; + } + final FragmentActivity fragmentActivity = getActivity(); + if (fragmentActivity instanceof ConfirmDialogFragmentCallback) { + callback = (ConfirmDialogFragmentCallback) fragmentActivity; } - this.context = context; } @NonNull diff --git a/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java index 478837ad..bbf2e35e 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java @@ -2,8 +2,10 @@ package awais.instagrabber.dialogs; import android.app.Dialog; import android.content.Context; -import android.content.pm.PackageManager; +import android.content.Intent; +import android.os.Build; import android.os.Bundle; +import android.provider.DocumentsContract; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; @@ -14,27 +16,26 @@ import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.FragmentTransaction; -import java.io.File; import java.time.format.DateTimeFormatter; import java.time.LocalDateTime; import java.util.Locale; import awais.instagrabber.databinding.DialogCreateBackupBinding; -import awais.instagrabber.utils.DirectoryChooser; +import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.ExportImportUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; -import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_PATH; -import static awais.instagrabber.utils.DownloadUtils.PERMS; +import static android.app.Activity.RESULT_OK; public class CreateBackupDialogFragment extends DialogFragment { + private static final String TAG = CreateBackupDialogFragment.class.getSimpleName(); private static final int STORAGE_PERM_REQUEST_CODE = 8020; private static final DateTimeFormatter BACKUP_FILE_DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss", Locale.US); + private static final int CREATE_FILE_REQUEST_CODE = 1; + private final OnResultListener onResultListener; private DialogCreateBackupBinding binding; @@ -113,59 +114,112 @@ public class CreateBackupDialogFragment extends DialogFragment { imm.hideSoftInputFromWindow(binding.etPassword.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN); }); binding.btnSaveTo.setOnClickListener(v -> { - if (ContextCompat.checkSelfPermission(context, PERMS[0]) == PackageManager.PERMISSION_GRANTED) { - showChooser(context); - } else { - requestPermissions(PERMS, STORAGE_PERM_REQUEST_CODE); - } + createFile(); + // if (ContextCompat.checkSelfPermission(context, PERMS[0]) == PackageManager.PERMISSION_GRANTED) { + // showChooser(context); + // } else { + // requestPermissions(PERMS, STORAGE_PERM_REQUEST_CODE); + // } }); } @Override public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == STORAGE_PERM_REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - final Context context = getContext(); - if (context == null) return; - showChooser(context); - } + // if (requestCode == STORAGE_PERM_REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // final Context context = getContext(); + // if (context == null) return; + // showChooser(context); + // } } - private void showChooser(@NonNull final Context context) { - final String folderPath = Utils.settingsHelper.getString(FOLDER_PATH); + @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 != CREATE_FILE_REQUEST_CODE) return; + final Context context = getContext(); + if (context == null) return; final Editable passwordText = binding.etPassword.getText(); final String password = binding.cbPassword.isChecked() && passwordText != null && !TextUtils.isEmpty(passwordText.toString()) ? passwordText.toString().trim() : null; - final DirectoryChooser directoryChooser = new DirectoryChooser() - .setInitialDirectory(folderPath) - .setInteractionListener(path -> { - final File file = new File(path, String.format("barinsta_%s.backup", LocalDateTime.now().format(BACKUP_FILE_DATE_TIME_FORMAT))); - int flags = 0; - if (binding.cbExportFavorites.isChecked()) { - flags |= ExportImportUtils.FLAG_FAVORITES; - } - if (binding.cbExportSettings.isChecked()) { - flags |= ExportImportUtils.FLAG_SETTINGS; - } - if (binding.cbExportLogins.isChecked()) { - flags |= ExportImportUtils.FLAG_COOKIES; - } - ExportImportUtils.exportData(context, flags, file, password, result -> { - if (onResultListener != null) { - onResultListener.onResult(result); - } - dismiss(); - }); - - }); - directoryChooser.setEnterTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); - directoryChooser.setExitTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); - directoryChooser.show(getChildFragmentManager(), "directory_chooser"); + int flags = 0; + if (binding.cbExportFavorites.isChecked()) { + flags |= ExportImportUtils.FLAG_FAVORITES; + } + if (binding.cbExportSettings.isChecked()) { + flags |= ExportImportUtils.FLAG_SETTINGS; + } + if (binding.cbExportLogins.isChecked()) { + flags |= ExportImportUtils.FLAG_COOKIES; + } + ExportImportUtils.exportData(context, flags, data.getData(), password, result -> { + if (onResultListener != null) { + onResultListener.onResult(result); + } + dismiss(); + }); + // try (final OutputStream stream = context.getContentResolver().openOutputStream(data.getData())) { + // } catch (Exception e) { + // Log.e(TAG, "onActivityResult: ", e); + // } } + // private void showChooser(@NonNull final Context context) { + // final String folderPath = Utils.settingsHelper.getString(FOLDER_PATH); + // final Editable passwordText = binding.etPassword.getText(); + // final String password = binding.cbPassword.isChecked() + // && passwordText != null + // && !TextUtils.isEmpty(passwordText.toString()) + // ? passwordText.toString().trim() + // : null; + // final DirectoryChooser directoryChooser = new DirectoryChooser() + // .setInitialDirectory(folderPath) + // .setInteractionListener(path -> { + // final Date now = new Date(); + // final File file = new File(path, String.format("barinsta_%s.backup", BACKUP_FILE_DATE_TIME_FORMAT.format(now))); + // int flags = 0; + // if (binding.cbExportFavorites.isChecked()) { + // flags |= ExportImportUtils.FLAG_FAVORITES; + // } + // if (binding.cbExportSettings.isChecked()) { + // flags |= ExportImportUtils.FLAG_SETTINGS; + // } + // if (binding.cbExportLogins.isChecked()) { + // flags |= ExportImportUtils.FLAG_COOKIES; + // } + // ExportImportUtils.exportData(context, flags, file, password, result -> { + // if (onResultListener != null) { + // onResultListener.onResult(result); + // } + // dismiss(); + // }); + // + // }); + // directoryChooser.setEnterTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + // directoryChooser.setExitTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + // directoryChooser.show(getChildFragmentManager(), "directory_chooser"); + // } + + private void createFile() { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("application/octet-stream"); + final String fileName = String.format("barinsta_%s.backup", LocalDateTime.now().format(BACKUP_FILE_DATE_TIME_FORMAT)); + intent.putExtra(Intent.EXTRA_TITLE, fileName); + + // 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, DownloadUtils.getBackupsDir().getUri()); + } + + startActivityForResult(intent, CREATE_FILE_REQUEST_CODE); + } + + public interface OnResultListener { void onResult(boolean result); } diff --git a/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java index 94a67ec7..6db8a875 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; @@ -16,7 +15,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,7 +23,7 @@ import com.facebook.drawee.controller.BaseControllerListener; import com.facebook.drawee.interfaces.DraweeController; import com.facebook.imagepipeline.image.ImageInfo; -import java.io.File; +// import java.io.File; import awais.instagrabber.R; import awais.instagrabber.customviews.drawee.AnimatedZoomableController; @@ -36,6 +35,7 @@ import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; import awais.instagrabber.webservices.UserRepository; import kotlinx.coroutines.Dispatchers; @@ -114,11 +114,11 @@ public class ProfilePicDialogFragment extends DialogFragment { binding.download.setOnClickListener(v -> { final Context context = getContext(); if (context == null) return; - if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) { - downloadProfilePicture(); - return; - } - requestPermissions(DownloadUtils.PERMS, 8020); + // if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) { + downloadProfilePicture(); + // return; + // } + // requestPermissions(DownloadUtils.PERMS, 8020); }); } @@ -195,14 +195,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/dialogs/RestoreBackupDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java index 7ab5f0fd..c4d8ea87 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java @@ -1,12 +1,18 @@ package awais.instagrabber.dialogs; import android.app.Dialog; +import android.content.ContentResolver; import android.content.Context; -import android.content.pm.PackageManager; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; +import android.provider.MediaStore; import android.text.Editable; import android.text.TextWatcher; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -15,30 +21,28 @@ import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.FragmentTransaction; - -import java.io.File; import awais.instagrabber.databinding.DialogRestoreBackupBinding; -import awais.instagrabber.utils.DirectoryChooser; +import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.ExportImportUtils; import awais.instagrabber.utils.PasswordUtils.IncorrectPasswordException; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; -import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_PATH; -import static awais.instagrabber.utils.DownloadUtils.PERMS; +import static android.app.Activity.RESULT_OK; public class RestoreBackupDialogFragment extends DialogFragment { + private static final String TAG = RestoreBackupDialogFragment.class.getSimpleName(); private static final int STORAGE_PERM_REQUEST_CODE = 8020; + private static final int OPEN_FILE_REQUEST_CODE = 1; private OnResultListener onResultListener; private DialogRestoreBackupBinding binding; - private File file; + // private File file; private boolean isEncrypted; + private Uri uri; public RestoreBackupDialogFragment() {} @@ -83,18 +87,62 @@ public class RestoreBackupDialogFragment extends DialogFragment { @Override public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == STORAGE_PERM_REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - showChooser(); + // if (requestCode == STORAGE_PERM_REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // showChooser(); + // } + } + + @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 != OPEN_FILE_REQUEST_CODE) return; + final Context context = getContext(); + if (context == null) return; + isEncrypted = ExportImportUtils.isEncrypted(context, data.getData()); + if (isEncrypted) { + binding.passwordGroup.setVisibility(View.VISIBLE); + binding.passwordGroup.post(() -> { + binding.etPassword.requestFocus(); + binding.etPassword.post(() -> { + final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) return; + imm.showSoftInput(binding.etPassword, InputMethodManager.SHOW_IMPLICIT); + }); + binding.btnRestore.setEnabled(!TextUtils.isEmpty(binding.etPassword.getText())); + }); + } else { + binding.passwordGroup.setVisibility(View.GONE); + binding.btnRestore.setEnabled(true); } + uri = data.getData(); + AppExecutors.INSTANCE.getMainThread().execute(() -> { + Cursor c = null; + try { + String[] projection = {MediaStore.Files.FileColumns.DISPLAY_NAME}; + final ContentResolver contentResolver = context.getContentResolver(); + c = contentResolver.query(uri, projection, null, null, null); + if (c != null) { + while (c.moveToNext()) { + final String displayName = c.getString(0); + binding.filePath.setText(displayName); + } + } + } catch (Exception e) { + Log.e(TAG, "onActivityResult: ", e); + } finally { + if (c != null) { + c.close(); + } + } + }); } private void init() { final Context context = getContext(); - if (context == null) { - return; - } + if (context == null) return; binding.btnRestore.setEnabled(false); - binding.btnRestore.setOnClickListener(v -> new Handler().post(() -> { + binding.btnRestore.setOnClickListener(v -> new Handler(Looper.getMainLooper()).post(() -> { + if (uri == null) return; int flags = 0; if (binding.cbFavorites.isChecked()) { flags |= ExportImportUtils.FLAG_FAVORITES; @@ -111,7 +159,7 @@ public class RestoreBackupDialogFragment extends DialogFragment { ExportImportUtils.importData( context, flags, - file, + uri, !isEncrypted ? null : text.toString(), result -> { if (onResultListener != null) { @@ -137,46 +185,56 @@ public class RestoreBackupDialogFragment extends DialogFragment { @Override public void afterTextChanged(final Editable s) {} }); - if (ContextCompat.checkSelfPermission(context, PERMS[0]) == PackageManager.PERMISSION_GRANTED) { - showChooser(); - return; - } - requestPermissions(PERMS, STORAGE_PERM_REQUEST_CODE); - } + // if (ContextCompat.checkSelfPermission(context, PERMS[0]) == PackageManager.PERMISSION_GRANTED) { + // showChooser(); + // return; + // } + // requestPermissions(PERMS, STORAGE_PERM_REQUEST_CODE); + final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + // intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + // intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{ + // "application/pdf", // .pdf + // "application/vnd.oasis.opendocument.text", // .odt + // "text/plain" // .txt + // }); + startActivityForResult(intent, OPEN_FILE_REQUEST_CODE); - private void showChooser() { - final String folderPath = Utils.settingsHelper.getString(FOLDER_PATH); - final Context context = getContext(); - if (context == null) return; - final DirectoryChooser directoryChooser = new DirectoryChooser() - .setInitialDirectory(folderPath) - .setShowBackupFiles(true) - .setInteractionListener(file -> { - isEncrypted = ExportImportUtils.isEncrypted(file); - if (isEncrypted) { - binding.passwordGroup.setVisibility(View.VISIBLE); - binding.passwordGroup.post(() -> { - binding.etPassword.requestFocus(); - binding.etPassword.post(() -> { - final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm == null) return; - imm.showSoftInput(binding.etPassword, InputMethodManager.SHOW_IMPLICIT); - }); - binding.btnRestore.setEnabled(!TextUtils.isEmpty(binding.etPassword.getText())); - }); - } else { - binding.passwordGroup.setVisibility(View.GONE); - binding.btnRestore.setEnabled(true); - } - this.file = file; - binding.filePath.setText(file.getAbsolutePath()); - }); - directoryChooser.setEnterTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); - directoryChooser.setExitTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); - directoryChooser.setOnCancelListener(this::dismiss); - directoryChooser.show(getChildFragmentManager(), "directory_chooser"); } + // private void showChooser() { + // final String folderPath = Utils.settingsHelper.getString(FOLDER_PATH); + // final Context context = getContext(); + // if (context == null) return; + // final DirectoryChooser directoryChooser = new DirectoryChooser() + // .setInitialDirectory(folderPath) + // .setShowBackupFiles(true) + // .setInteractionListener(file -> { + // isEncrypted = ExportImportUtils.isEncrypted(file); + // if (isEncrypted) { + // binding.passwordGroup.setVisibility(View.VISIBLE); + // binding.passwordGroup.post(() -> { + // binding.etPassword.requestFocus(); + // binding.etPassword.post(() -> { + // final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + // if (imm == null) return; + // imm.showSoftInput(binding.etPassword, InputMethodManager.SHOW_IMPLICIT); + // }); + // binding.btnRestore.setEnabled(!TextUtils.isEmpty(binding.etPassword.getText())); + // }); + // } else { + // binding.passwordGroup.setVisibility(View.GONE); + // binding.btnRestore.setEnabled(true); + // } + // this.file = file; + // binding.filePath.setText(file.getAbsolutePath()); + // }); + // directoryChooser.setEnterTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + // directoryChooser.setExitTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + // directoryChooser.setOnCancelListener(this::dismiss); + // directoryChooser.show(getChildFragmentManager(), "directory_chooser"); + // } + public interface OnResultListener { void onResult(boolean result); } diff --git a/app/src/main/java/awais/instagrabber/fragments/CollectionPostsFragment.java b/app/src/main/java/awais/instagrabber/fragments/CollectionPostsFragment.java index 1480f6e3..05212d19 100644 --- a/app/src/main/java/awais/instagrabber/fragments/CollectionPostsFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/CollectionPostsFragment.java @@ -27,7 +27,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.content.PermissionChecker; import androidx.core.graphics.ColorUtils; import androidx.fragment.app.Fragment; import androidx.navigation.NavController; @@ -65,9 +64,6 @@ import awais.instagrabber.utils.Utils; import awais.instagrabber.webservices.CollectionService; import awais.instagrabber.webservices.ServiceCallback; -import static androidx.core.content.PermissionChecker.checkSelfPermission; -import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; - public class CollectionPostsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "CollectionPostsFragment"; private static final int STORAGE_PERM_REQUEST_CODE = 8020; @@ -106,12 +102,12 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay if (CollectionPostsFragment.this.selectedFeedModels == null) return false; final Context context = getContext(); if (context == null) return false; - if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { - DownloadUtils.download(context, ImmutableList.copyOf(CollectionPostsFragment.this.selectedFeedModels)); - binding.posts.endSelection(); - return true; - } - requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION); + // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { + DownloadUtils.download(context, ImmutableList.copyOf(CollectionPostsFragment.this.selectedFeedModels)); + binding.posts.endSelection(); + // return true; + // } + // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION); } return false; } @@ -141,13 +137,13 @@ public class CollectionPostsFragment extends Fragment implements SwipeRefreshLay public void onDownloadClick(final Media feedModel, final int childPosition) { final Context context = getContext(); if (context == null) return; - if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { - DownloadUtils.showDownloadDialog(context, feedModel, childPosition); - return; - } - downloadFeedModel = feedModel; - downloadChildPosition = -1; - requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); + // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { + DownloadUtils.showDownloadDialog(context, feedModel, childPosition); + // return; + // } + // downloadFeedModel = feedModel; + // downloadChildPosition = -1; + // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); } @Override diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index 6efd3711..873448da 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -105,7 +105,7 @@ import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.PostViewV2ViewModel; -import static androidx.core.content.PermissionChecker.checkSelfPermission; +//import static androidx.core.content.PermissionChecker.checkSelfPermission; import static awais.instagrabber.fragments.HashTagFragment.ARG_HASHTAG; import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_SHOWN_COUNT_TOOLTIP; import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; @@ -119,6 +119,7 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme private static final int STORAGE_PERM_REQUEST_CODE = 8020; private DialogPostViewBinding binding; + private Context context; private boolean detailsVisible = true; private boolean video; private VideoPlayerViewHelper videoPlayerViewHelper; @@ -211,6 +212,12 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme init(); } + @Override + public void onAttach(@NonNull final Context context) { + super.onAttach(context); + this.context = context; + } + @Override public void onPause() { super.onPause(); @@ -454,13 +461,7 @@ public class PostViewV2Fragment extends Fragment implements EditTextDialogFragme private void setupDownload() { bottom.download.setOnClickListener(v -> { - final Context context = getContext(); - if (context == null) return; - if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { - DownloadUtils.showDownloadDialog(context, viewModel.getMedia(), sliderPosition); - return; - } - requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); + DownloadUtils.showDownloadDialog(context, viewModel.getMedia(), sliderPosition); }); TooltipCompat.setTooltipText(bottom.download, getString(R.string.action_download)); } diff --git a/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java index 8ae5a963..d366a476 100644 --- a/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/SavedViewerFragment.java @@ -19,7 +19,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.PermissionChecker; import androidx.fragment.app.Fragment; import androidx.navigation.NavController; import androidx.navigation.NavDirections; @@ -46,8 +45,6 @@ import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; -import static androidx.core.content.PermissionChecker.checkSelfPermission; -import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; import static awais.instagrabber.utils.Utils.settingsHelper; public final class SavedViewerFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { @@ -88,12 +85,12 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL if (SavedViewerFragment.this.selectedFeedModels == null) return false; final Context context = getContext(); if (context == null) return false; - if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { - DownloadUtils.download(context, ImmutableList.copyOf(SavedViewerFragment.this.selectedFeedModels)); - binding.posts.endSelection(); - return true; - } - requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION); + // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { + DownloadUtils.download(context, ImmutableList.copyOf(SavedViewerFragment.this.selectedFeedModels)); + binding.posts.endSelection(); + // return true; + // } + // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION); } return false; } @@ -123,13 +120,13 @@ public final class SavedViewerFragment extends Fragment implements SwipeRefreshL public void onDownloadClick(final Media feedModel, final int childPosition) { final Context context = getContext(); if (context == null) return; - if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { - DownloadUtils.showDownloadDialog(context, feedModel, childPosition); - return; - } - downloadFeedModel = feedModel; - downloadChildPosition = childPosition; - requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); + // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { + DownloadUtils.showDownloadDialog(context, feedModel, childPosition); + // return; + // } + // downloadFeedModel = feedModel; + // downloadChildPosition = childPosition; + // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); } @Override diff --git a/app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java b/app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java index 76374891..9e4474a1 100644 --- a/app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/TopicPostsFragment.java @@ -24,7 +24,6 @@ import androidx.activity.OnBackPressedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.content.PermissionChecker; import androidx.core.graphics.ColorUtils; import androidx.fragment.app.Fragment; import androidx.navigation.NavController; @@ -60,9 +59,6 @@ import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.webservices.DiscoverService; -import static androidx.core.content.PermissionChecker.checkSelfPermission; -import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; - public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = TopicPostsFragment.class.getSimpleName(); private static final int STORAGE_PERM_REQUEST_CODE = 8020; @@ -99,12 +95,12 @@ public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.O if (TopicPostsFragment.this.selectedFeedModels == null) return false; final Context context = getContext(); if (context == null) return false; - if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { - DownloadUtils.download(context, ImmutableList.copyOf(TopicPostsFragment.this.selectedFeedModels)); - binding.posts.endSelection(); - return true; - } - requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION); + // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { + DownloadUtils.download(context, ImmutableList.copyOf(TopicPostsFragment.this.selectedFeedModels)); + binding.posts.endSelection(); + return true; + // } + // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE_FOR_SELECTION); } return false; } @@ -134,13 +130,13 @@ public class TopicPostsFragment extends Fragment implements SwipeRefreshLayout.O public void onDownloadClick(final Media feedModel, final int childPosition) { final Context context = getContext(); if (context == null) return; - if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { - DownloadUtils.showDownloadDialog(context, feedModel, childPosition); - return; - } - downloadFeedModel = feedModel; - downloadChildPosition = -1; - requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); + // if (checkSelfPermission(context, WRITE_PERMISSION) == PermissionChecker.PERMISSION_GRANTED) { + DownloadUtils.showDownloadDialog(context, feedModel, childPosition); + // return; + // } + // downloadFeedModel = feedModel; + // downloadChildPosition = -1; + // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); } @Override diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java index 450b21e7..7c705042 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -1408,13 +1408,13 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); return; } - if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) { - DownloadUtils.download(context, media); - Toast.makeText(context, R.string.downloader_downloading_media, Toast.LENGTH_SHORT).show(); - return; - } - tempMedia = media; - requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); + // if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) == PackageManager.PERMISSION_GRANTED) { + DownloadUtils.download(context, media); + Toast.makeText(context, R.string.downloader_downloading_media, Toast.LENGTH_SHORT).show(); + // return; + // } + // tempMedia = media; + // requestPermissions(DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE); } @Nullable diff --git a/app/src/main/java/awais/instagrabber/fragments/imageedit/ImageEditFragment.java b/app/src/main/java/awais/instagrabber/fragments/imageedit/ImageEditFragment.java index 7586bdce..034b6706 100644 --- a/app/src/main/java/awais/instagrabber/fragments/imageedit/ImageEditFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/imageedit/ImageEditFragment.java @@ -183,11 +183,12 @@ public class ImageEditFragment extends Fragment { if (context == null) return; final Uri resultUri = viewModel.getResultUri().getValue(); if (resultUri == null) return; - Utils.mediaScanFile(context, new File(resultUri.toString()), (path, uri) -> AppExecutors.INSTANCE.getMainThread().execute(() -> { + AppExecutors.INSTANCE.getMainThread().execute(() -> { final NavController navController = NavHostFragment.findNavController(this); setNavControllerResult(navController, resultUri); navController.navigateUp(); - })); + }); + // Utils.mediaScanFile(context, new File(resultUri.toString()), (path, uri) -> ); }); } 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 6a18c69a..3f76ffcc 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java @@ -1,27 +1,41 @@ package awais.instagrabber.fragments.settings; import android.content.Context; -import android.view.View; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; +import android.util.Log; import androidx.annotation.NonNull; -import androidx.appcompat.widget.AppCompatButton; -import androidx.appcompat.widget.AppCompatTextView; +import androidx.annotation.Nullable; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import androidx.preference.PreferenceViewHolder; import androidx.preference.SwitchPreferenceCompat; -import com.google.android.material.switchmaterial.SwitchMaterial; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import awais.instagrabber.R; -import awais.instagrabber.utils.DirectoryChooser; +import awais.instagrabber.dialogs.ConfirmDialogFragment; +import awais.instagrabber.utils.AppExecutors; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; -import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_PATH; -import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_SAVE_TO; +import static android.app.Activity.RESULT_OK; +import static awais.instagrabber.activities.DirectorySelectActivity.SELECT_DIR_REQUEST_CODE; import static awais.instagrabber.utils.Utils.settingsHelper; public class DownloadsPreferencesFragment extends BasePreferencesFragment { + private static final String TAG = DownloadsPreferencesFragment.class.getSimpleName(); + // private SaveToCustomFolderPreference.ResultCallback resultCallback; + @Override void setupPreferenceScreen(final PreferenceScreen screen) { final Context context = getContext(); @@ -40,13 +54,88 @@ public class DownloadsPreferencesFragment extends BasePreferencesFragment { } 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)); + final Preference preference = new Preference(context); + preference.setKey(PreferenceKeys.PREF_BARINSTA_DIR_URI); + preference.setIconSpaceReserved(false); + preference.setTitle(R.string.barinsta_folder); + preference.setSummaryProvider(p -> { + final String currentValue = settingsHelper.getString(PreferenceKeys.PREF_BARINSTA_DIR_URI); + if (TextUtils.isEmpty(currentValue)) return ""; + String path; + try { + path = URLDecoder.decode(currentValue, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + path = currentValue; + } + return path; + }); + preference.setOnPreferenceClickListener(p -> { + openDirectoryChooser(DownloadUtils.getRootDirUri()); + return true; + }); + return preference; + // return new SaveToCustomFolderPreference(context, checked -> { + // try { + // DownloadUtils.init(context); + // } catch (DownloadUtils.ReselectDocumentTreeException e) { + // if (!checked) return; + // startDocumentSelector(e.getInitialUri()); + // } catch (Exception e) { + // Log.e(TAG, "getSaveToCustomFolderPreference: ", e); + // } + // }, (resultCallback) -> { + // // Choose a directory using the system's file picker. + // startDocumentSelector(null); + // 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); + // }); + } + + private void openDirectoryChooser(final Uri initialUri) { + final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialUri != null) { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri); + } + startActivityForResult(intent, SELECT_DIR_REQUEST_CODE); + } + + @Override + public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) { + if (requestCode != SELECT_DIR_REQUEST_CODE) return; + if (resultCode != RESULT_OK) return; + if (data == null || data.getData() == null) return; + final Context context = getContext(); + if (context == null) return; + AppExecutors.INSTANCE.getMainThread().execute(() -> { + try { + Utils.setupSelectedDir(context, data); + } catch (Exception e) { + // Should not come to this point. + // If it does, we have to show this error to the user so that they can report it. + try (final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw)) { + e.printStackTrace(pw); + final ConfirmDialogFragment dialogFragment = ConfirmDialogFragment.newInstance( + 123, + R.string.error, + "Please report this error to the developers:\n\n" + sw.toString(), + R.string.ok, + 0, + 0 + ); + dialogFragment.show(getChildFragmentManager(), ConfirmDialogFragment.class.getSimpleName()); + } catch (IOException ioException) { + Log.e(TAG, "onActivityResult: ", ioException); + } + } + }, 500); } private Preference getPrependUsernameToFilenamePreference(@NonNull final Context context) { @@ -57,53 +146,74 @@ public class DownloadsPreferencesFragment extends BasePreferencesFragment { return preference; } - 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 = PreferenceKeys.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); - } - } + // public static class SaveToCustomFolderPreference extends Preference { + // private AppCompatTextView customPathTextView; + // private final OnSaveToChangeListener onSaveToChangeListener; + // private final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener; + // private final String key; + // + // public SaveToCustomFolderPreference(final Context context, + // final OnSaveToChangeListener onSaveToChangeListener, + // final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener) { + // super(context); + // this.onSaveToChangeListener = onSaveToChangeListener; + // 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 Context context = getContext(); + // String customPath = settingsHelper.getString(FOLDER_PATH); + // if (!TextUtils.isEmpty(customPath) && customPath.startsWith("content") && context != null) { + // final Uri uri = Uri.parse(customPath); + // final DocumentFile documentFile = DocumentFile.fromSingleUri(context, uri); + // try { + // customPath = Utils.getDocumentFileRealPath(context, documentFile).getAbsolutePath(); + // } catch (Exception e) { + // Log.e(TAG, "onBindViewHolder: ", e); + // } + // } + // customPathTextView.setText(customPath); + // if (onSaveToChangeListener != null) { + // onSaveToChangeListener.onChange(isChecked); + // } + // }); + // 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); + // } + // + // public interface OnSaveToChangeListener { + // void onChange(boolean checked); + // } + // } } diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.kt b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.kt index bdc8a77c..bf1b7edf 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.kt +++ b/app/src/main/java/awais/instagrabber/fragments/settings/PreferenceKeys.kt @@ -19,6 +19,7 @@ object PreferenceKeys { const val APP_THEME = "app_theme_v19" const val APP_LANGUAGE = "app_language_v19" const val STORY_SORT = "story_sort" + const val PREF_BARINSTA_DIR_URI = "barinsta_dir_uri" // set string prefs const val KEYWORD_FILTERS = "keyword_filters" 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.kt b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt index 379ea8db..4d6abced 100644 --- a/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt +++ b/app/src/main/java/awais/instagrabber/utils/BitmapUtils.kt @@ -8,6 +8,7 @@ import android.net.Uri import android.util.Log import android.util.LruCache import androidx.core.util.Pair +import androidx.documentfile.provider.DocumentFile import awais.instagrabber.utils.extensions.TAG import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -192,9 +193,9 @@ object BitmapUtils { } @Throws(IOException::class) - fun convertToJpegAndSaveToFile(bitmap: Bitmap, file: File?): File { - val tempFile = file ?: DownloadUtils.getTempFile() - FileOutputStream(tempFile).use { output -> + fun convertToJpegAndSaveToFile(contentResolver: ContentResolver, bitmap: Bitmap, file: DocumentFile?): DocumentFile { + val tempFile = file ?: DownloadUtils.getTempFile(null, "jpg") + contentResolver.openOutputStream(tempFile.uri).use { output -> val compressResult = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output) if (!compressResult) { throw RuntimeException("Compression failed!") diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.kt b/app/src/main/java/awais/instagrabber/utils/Constants.kt index 8cf96e03..82b1f87e 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.kt +++ b/app/src/main/java/awais/instagrabber/utils/Constants.kt @@ -89,4 +89,5 @@ object Constants { const val DM_THREAD_ACTION_EXTRA_THREAD_ID = "thread_id" const val DM_THREAD_ACTION_EXTRA_THREAD_TITLE = "thread_title" const val X_IG_APP_ID = "936619743392459" + const val EXTRA_INITIAL_URI = "initial_uri" } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/DirectoryChooser.java b/app/src/main/java/awais/instagrabber/utils/DirectoryChooser.java index d349e490..1ea880a5 100755 --- a/app/src/main/java/awais/instagrabber/utils/DirectoryChooser.java +++ b/app/src/main/java/awais/instagrabber/utils/DirectoryChooser.java @@ -3,7 +3,6 @@ package awais.instagrabber.utils; import android.app.Activity; import android.app.Dialog; import android.content.Context; -import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Environment; @@ -11,19 +10,14 @@ import android.os.FileObserver; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; -import com.google.android.material.snackbar.BaseTransientBottomBar; -import com.google.android.material.snackbar.Snackbar; - import java.io.File; import java.util.ArrayList; import java.util.Collections; @@ -95,15 +89,15 @@ public final class DirectoryChooser extends DialogFragment { if (context == null) context = getContext(); if (context == null) context = getActivity(); if (context == null) return; - if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) != PackageManager.PERMISSION_GRANTED) { - final String text = "Storage permissions denied!"; - if (container == null) { - Toast.makeText(context, text, Toast.LENGTH_LONG).show(); - } else { - Snackbar.make(container, text, BaseTransientBottomBar.LENGTH_LONG).show(); - } - dismiss(); - } + // if (ContextCompat.checkSelfPermission(context, DownloadUtils.PERMS[0]) != PackageManager.PERMISSION_GRANTED) { + // final String text = "Storage permissions denied!"; + // if (container == null) { + // Toast.makeText(context, text, Toast.LENGTH_LONG).show(); + // } else { + // Snackbar.make(container, text, BaseTransientBottomBar.LENGTH_LONG).show(); + // } + // dismiss(); + // } final View.OnClickListener clickListener = v -> { if (v == binding.btnConfirm) { if (interactionListener != null && isValidFile(selectedDir)) diff --git a/app/src/main/java/awais/instagrabber/utils/DirectoryUtils.java b/app/src/main/java/awais/instagrabber/utils/DirectoryUtils.java index 1ad6a531..205239c1 100644 --- a/app/src/main/java/awais/instagrabber/utils/DirectoryUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/DirectoryUtils.java @@ -1,7 +1,5 @@ package awais.instagrabber.utils; -import android.content.Context; -import android.os.Build; import android.os.Environment; import java.io.File; @@ -10,8 +8,6 @@ import java.util.HashSet; import java.util.Set; import java.util.regex.Pattern; -import awais.instagrabber.R; - public class DirectoryUtils { private static final Pattern DIR_SEPORATOR = Pattern.compile("/"); @@ -73,22 +69,22 @@ public class DirectoryUtils { return rv; } - public static File getOutputMediaDirectory(final Context context, final String... dirs) { - if (context == null) return null; - final File[] externalMediaDirs = context.getExternalMediaDirs(); - if (externalMediaDirs == null || externalMediaDirs.length == 0) return context.getFilesDir(); - final File externalMediaDir = externalMediaDirs[0]; - File subDir = new File(externalMediaDir, context.getString(R.string.app_name)); - if (dirs != null) { - for (final String dir : dirs) { - subDir = new File(subDir, dir); - //noinspection ResultOfMethodCallIgnored - subDir.mkdirs(); - } - } - if (!subDir.exists()) { - return context.getFilesDir(); - } - return subDir; - } + // public static File getOutputMediaDirectory(final Context context, final String... dirs) { + // if (context == null) return null; + // final File[] externalMediaDirs = context.getExternalMediaDirs(); + // if (externalMediaDirs == null || externalMediaDirs.length == 0) return context.getFilesDir(); + // final File externalMediaDir = externalMediaDirs[0]; + // File subDir = new File(externalMediaDir, context.getString(R.string.app_name)); + // if (dirs != null) { + // for (final String dir : dirs) { + // subDir = new File(subDir, dir); + // //noinspection ResultOfMethodCallIgnored + // subDir.mkdirs(); + // } + // } + // if (!subDir.exists()) { + // return context.getFilesDir(); + // } + // return subDir; + // } } diff --git a/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java b/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java index be24dd48..755b6198 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.content.UriPermission; +import android.Manifest; +import android.net.Uri; 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; @@ -42,51 +46,159 @@ import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.VideoVersion; import awais.instagrabber.workers.DownloadWorker; -import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_PATH; -import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_SAVE_TO; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_BARINSTA_DIR_URI; public final class DownloadUtils { - private static final String TAG = "DownloadUtils"; + private static final String TAG = DownloadUtils.class.getSimpleName(); + // 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 DIR_RECORDINGS = "Sent Recordings"; + private static final String DIR_TEMP = "Temp"; + private static final String DIR_BACKUPS = "Backups"; + + private static DocumentFile root; public static final String WRITE_PERMISSION = Manifest.permission.WRITE_EXTERNAL_STORAGE; public static final String[] PERMS = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; - @NonNull - private static File getDownloadDir() { - File dir = new File(Environment.getExternalStorageDirectory(), "Download"); + public static void init(@NonNull final Context context) throws ReselectDocumentTreeException { + final String barinstaDirUri = Utils.settingsHelper.getString(PREF_BARINSTA_DIR_URI); + if (TextUtils.isEmpty(barinstaDirUri)) { + throw new ReselectDocumentTreeException("folder path is null or empty"); + } + if (!barinstaDirUri.startsWith("content")) { + // reselect the folder in selector view + throw new ReselectDocumentTreeException(Uri.parse(barinstaDirUri)); + } + final Uri uri = Uri.parse(barinstaDirUri); + final List existingPermissions = context.getContentResolver().getPersistedUriPermissions(); + if (existingPermissions.isEmpty()) { + // reselect the folder in selector view + throw new ReselectDocumentTreeException(uri); + } + final boolean anyMatch = existingPermissions.stream().anyMatch(uriPermission -> uriPermission.getUri().equals(uri)); + if (!anyMatch) { + // reselect the folder in selector view + throw new ReselectDocumentTreeException(uri); + } + root = DocumentFile.fromTreeUri(context, uri); + if (root == null || !root.exists() || root.lastModified() == 0) { + root = null; + throw new ReselectDocumentTreeException(uri); + } + } - if (Utils.settingsHelper.getBoolean(FOLDER_SAVE_TO)) { - final String customPath = Utils.settingsHelper.getString(FOLDER_PATH); - if (!TextUtils.isEmpty(customPath)) { - dir = new File(customPath); + public static void destroy() { + root = null; + } + + @Nullable + public static DocumentFile getDownloadDir(final String... dirs) { + if (root == null) { + return null; + } + DocumentFile subDir = root; + if (dirs != null) { + for (final String dir : dirs) { + if (subDir == null || TextUtils.isEmpty(dir)) continue; + final DocumentFile subDirFile = subDir.findFile(dir); + final boolean exists = subDirFile != null && subDirFile.exists(); + subDir = exists ? subDirFile : subDir.createDirectory(dir); } } - return dir; + return subDir; } @Nullable - private static File getDownloadDir(@NonNull final Context context, @Nullable final String username) { - return getDownloadDir(context, username, false); + 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()) { + // final boolean mkdirs = dir.mkdirs(); + // if (!mkdirs) { + // Log.e(TAG, "getDownloadDir: failed to create dir"); + // } + // } + // if (Utils.settingsHelper.getBoolean(FOLDER_SAVE_TO)) { + // final String customPath = Utils.settingsHelper.getString(FOLDER_PATH); + // if (!TextUtils.isEmpty(customPath)) { + // dir = new File(customPath); + // } + // } + return getDownloadDir(DIR_DOWNLOADS); } @Nullable - private static File getDownloadDir(final Context context, - @Nullable final String username, - final boolean skipCreateDir) { - File dir = getDownloadDir(); - - if (Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_USER_FOLDER) && !TextUtils.isEmpty(username)) { - final String finaleUsername = username.startsWith("@") ? username.substring(1) : username; - dir = new File(dir, finaleUsername); - } + public static DocumentFile getCameraDir() { + return getDownloadDir(DIR_CAMERA); + } - if (context != null && !skipCreateDir && !dir.exists() && !dir.mkdirs()) { + @Nullable + public static DocumentFile getImageEditDir(final String sessionId) { + return getDownloadDir(DIR_EDIT, sessionId); + } + + @Nullable + public static DocumentFile getRecordingsDir() { + return getDownloadDir(DIR_RECORDINGS); + } + + @Nullable + public static DocumentFile getBackupsDir() { + return getDownloadDir(DIR_BACKUPS); + } + + // @Nullable + // private static DocumentFile getDownloadDir(@NonNull final Context context, @Nullable final String username) { + // return getDownloadDir(context, username, false); + // } + + @Nullable + 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; + } + // 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(PreferenceKeys.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(DIR_TEMP); + if (file == null) { + file = root.createDirectory(DIR_TEMP); + } + return file; + } + // public static void dmDownload(@NonNull final Context context, // @Nullable final String username, // final String modelId, @@ -99,70 +211,82 @@ 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(); -// } - - @NonNull - private static File getDownloadSaveFile(final File finalDir, - final String postId, - final String displayUrl) { - return getDownloadSaveFile(finalDir, postId, "", displayUrl, ""); + // 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(); + // } + + private static Pair, String> getDownloadSavePaths(final List paths, + final String postId, + final String displayUrl) { + return getDownloadSavePaths(paths, postId, "", displayUrl, ""); } - @NonNull - private static File getDownloadSaveFile(final File finalDir, - final String postId, - final String displayUrl, - final String username) { - return getDownloadSaveFile(finalDir, postId, "", displayUrl, username); + private static Pair, String> getDownloadSavePaths(final List paths, + final String postId, + final String displayUrl, + final String username) { + return getDownloadSavePaths(paths, postId, "", displayUrl, username); } - private static File getDownloadChildSaveFile(final File downloadDir, - final String postId, - final int childPosition, - final String url, - final String username) { + private static Pair, String> getDownloadChildSavePaths(final List paths, + final String postId, + final int childPosition, + final String url, + final String username) { final String sliderPostfix = "_slide_" + childPosition; - return getDownloadSaveFile(downloadDir, postId, sliderPostfix, url, username); + return getDownloadSavePaths(paths, postId, sliderPostfix, url, username); } - @NonNull - private static File getDownloadSaveFile(final File finalDir, - final String postId, - final String sliderPostfix, - final String displayUrl, - final String username) { + private static Pair, String> getDownloadSavePaths(final List paths, + final String postId, + final String sliderPostfix, + final String displayUrl, + final String username) { + if (paths == null) return null; + final String extension = getFileExtensionFromUrl(displayUrl); final String usernamePrepend = TextUtils.isEmpty(username) ? "" : (username + "_"); - final String fileName = usernamePrepend + postId + sliderPostfix + getFileExtensionFromUrl(displayUrl); - return new File(finalDir, fileName); + final String fileName = usernamePrepend + 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() { - return getTempFile(null, null); - } + // 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; + final String mimeType1 = Utils.mimeTypeMap.getMimeTypeFromExtension(extension); + if (mimeType1 != null) { + mimeType = mimeType1; + } + } + DocumentFile file = dir.findFile(name); + if (file == null) { + file = dir.createFile(mimeType, name); } - return new File(dir, name); + return file; } /** @@ -212,14 +336,20 @@ public final class DownloadUtils { 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, ""); - final File usernamePrependedFile = getDownloadSaveFile(downloadDir, media.getCode(), url, username); - checkList.add(file.exists() || usernamePrependedFile.exists()); + final Pair, String> file = getDownloadSavePaths(new ArrayList<>(userFolderPaths), media.getCode(), url, ""); + final boolean fileExists = file.first != null && checkPathExists(file.first); + boolean usernameFileExists = false; + if (!fileExists) { + final Pair, String> usernameFile = getDownloadSavePaths( + new ArrayList<>(userFolderPaths), media.getCode(), url, username); + usernameFileExists = usernameFile.first != null && checkPathExists(usernameFile.first); + } + checkList.add(fileExists || usernameFileExists); break; } case MEDIA_TYPE_SLIDER: @@ -228,9 +358,16 @@ 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, ""); - final File usernamePrependedFile = getDownloadChildSaveFile(downloadDir, media.getCode(), i + 1, url, username); - checkList.add(file.exists() || usernamePrependedFile.exists()); + final Pair, String> file = getDownloadChildSavePaths( + new ArrayList<>(userFolderPaths), media.getCode(), i + 1, url, ""); + final boolean fileExists = file.first != null && checkPathExists(file.first); + boolean usernameFileExists = false; + if (!fileExists) { + final Pair, String> usernameFile = getDownloadChildSavePaths( + new ArrayList<>(userFolderPaths), media.getCode(), i + 1, url, username); + usernameFileExists = usernameFile.first != null && checkPathExists(usernameFile.first); + } + checkList.add(fileExists || usernameFileExists); } break; default: @@ -238,6 +375,18 @@ public final class DownloadUtils { return checkList; } + private static boolean checkPathExists(@NonNull final List paths) { + if (root == null) return false; + DocumentFile dir = root; + for (final String path : paths) { + dir = dir.findFile(path); + if (dir == null || !dir.exists()) { + return false; + } + } + return true; + } + public static void showDownloadDialog(@NonNull Context context, @NonNull final Media feedModel, final int childPosition) { @@ -272,17 +421,25 @@ 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()); + if (downloadDir == null) return; final String url = storyModel.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO ? storyModel.getVideoUrl() : storyModel.getStoryUrl(); + final String extension = DownloadUtils.getFileExtensionFromUrl(url); final String baseFileName = storyModel.getStoryMediaId() + "_" - + storyModel.getTimestamp() + DownloadUtils.getFileExtensionFromUrl(url); + + storyModel.getTimestamp() + extension; final String usernamePrepend = Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME) - && storyModel.getUsername() != null ? storyModel.getUsername() + "_" : ""; - final File saveFile = new File(downloadDir, - usernamePrepend + baseFileName); - download(context, url, saveFile.getAbsolutePath()); + && storyModel.getUsername() != null ? storyModel.getUsername() + "_" : ""; + final String fileName = usernamePrepend + baseFileName; + DocumentFile saveFile = downloadDir.findFile(fileName); + if (saveFile == null) { + final String mimeType = Utils.mimeTypeMap.getMimeTypeFromExtension(extension.startsWith(".") ? extension.substring(1) : extension); + if (mimeType == null) return; + saveFile = downloadDir.createFile(mimeType, fileName); + } + // final File saveFile = new File(downloadDir, fileName); + download(context, url, saveFile); } public static void download(@NonNull final Context context, @@ -304,11 +461,12 @@ 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 String username = mediaUser == null ? "" : mediaUser.getUsername(); + final List userFolderPaths = getSubPathForUserFolder(username); + // final DocumentFile downloadDir = getDownloadDir(context, mediaUser == null ? "" : mediaUser.getUsername()); switch (media.getMediaType()) { case MEDIA_TYPE_IMAGE: case MEDIA_TYPE_VIDEO: { @@ -323,8 +481,10 @@ public final class DownloadUtils { 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_VOICE: { @@ -333,29 +493,54 @@ 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 String usernamePrepend = Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME) && mediaUser != null ? mediaUser.getUsername() : ""; - final File file = getDownloadChildSaveFile(downloadDir, media.getCode(), i + 1, url, usernamePrepend); - map.put(url, file.getAbsolutePath()); + final String usernamePrepend = Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME) && mediaUser != null + ? mediaUser.getUsername() + : ""; + final Pair, String> pair = getDownloadChildSavePaths( + new ArrayList<>(userFolderPaths), media.getCode(), i + 1, url, usernamePrepend); + final DocumentFile file = createFile(pair); + if (file == null) continue; + map.put(url, file); } break; default: } } + if (map.isEmpty()) return; download(context, map); } + @Nullable + private static DocumentFile createFile(@NonNull final Pair, String> pair) { + if (root == null) return null; + 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()) { @@ -387,12 +572,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(); @@ -400,19 +586,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) @@ -421,4 +613,30 @@ public final class DownloadUtils { WorkManager.getInstance(context) .enqueue(downloadWorkRequest); } + + @Nullable + public static Uri getRootDirUri() { + return root != null ? root.getUri() : null; + } + + public static class ReselectDocumentTreeException extends Exception { + private final Uri initialUri; + + public ReselectDocumentTreeException() { + initialUri = null; + } + + public ReselectDocumentTreeException(final String message) { + super(message); + initialUri = null; + } + + public ReselectDocumentTreeException(final Uri initialUri) { + this.initialUri = initialUri; + } + + public Uri getInitialUri() { + return initialUri; + } + } } diff --git a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java index cc03534d..64d91cdd 100755 --- a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java @@ -2,6 +2,7 @@ package awais.instagrabber.utils; import android.content.Context; import android.content.SharedPreferences; +import android.net.Uri; import android.util.Base64; import android.util.Log; import android.util.Pair; @@ -20,9 +21,8 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; @@ -55,14 +55,15 @@ public final class ExportImportUtils { public static void importData(@NonNull final Context context, @ExportImportFlags final int flags, - @NonNull final File file, + @NonNull final Uri uri, final String password, final FetchListener fetchListener) throws IncorrectPasswordException { - try (final FileInputStream fis = new FileInputStream(file)) { - final int configType = fis.read(); + try (final InputStream stream = context.getContentResolver().openInputStream(uri)) { + if (stream == null) return; + final int configType = stream.read(); final StringBuilder builder = new StringBuilder(); int c; - while ((c = fis.read()) != -1) { + while ((c = stream.read()) != -1) { builder.append((char) c); } if (configType == 'A') { @@ -223,9 +224,11 @@ public final class ExportImportUtils { } } - public static boolean isEncrypted(final File file) { - try (final FileInputStream fis = new FileInputStream(file)) { - final int configType = fis.read(); + public static boolean isEncrypted(@NonNull final Context context, + @NonNull final Uri uri) { + try (final InputStream stream = context.getContentResolver().openInputStream(uri)) { + if (stream == null) return false; + final int configType = stream.read(); if (configType == 'A') { return true; } @@ -237,7 +240,7 @@ public final class ExportImportUtils { public static void exportData(@NonNull final Context context, @ExportImportFlags final int flags, - @NonNull final File filePath, + @NonNull final Uri uri, final String password, final FetchListener fetchListener) { getExportString(flags, context, exportString -> { @@ -258,15 +261,20 @@ public final class ExportImportUtils { exportBytes = Base64.encode(exportString.getBytes(), Base64.DEFAULT | Base64.NO_WRAP | Base64.NO_PADDING); } if (exportBytes != null && exportBytes.length > 1) { - try (final FileOutputStream fos = new FileOutputStream(filePath)) { - fos.write(isPass ? 'A' : 'Z'); - fos.write(exportBytes); + try (final OutputStream stream = context.getContentResolver().openOutputStream(uri)) { + if (stream == null) return; + stream.write(isPass ? 'A' : 'Z'); + stream.write(exportBytes); if (fetchListener != null) fetchListener.onResult(true); - } catch (final Exception e) { + } catch (Exception e) { if (fetchListener != null) fetchListener.onResult(false); if (BuildConfig.DEBUG) Log.e(TAG, "", e); } - } else if (fetchListener != null) fetchListener.onResult(false); + return; + } + if (fetchListener != null) { + fetchListener.onResult(false); + } }); } diff --git a/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt index 911b30c8..65fff446 100644 --- a/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt +++ b/app/src/main/java/awais/instagrabber/utils/MediaUploader.kt @@ -3,6 +3,7 @@ package awais.instagrabber.utils import android.content.ContentResolver import android.graphics.Bitmap import android.net.Uri +import androidx.documentfile.provider.DocumentFile import awais.instagrabber.models.UploadVideoOptions import awais.instagrabber.webservices.interceptors.AddCookiesInterceptor import kotlinx.coroutines.Dispatchers @@ -12,8 +13,6 @@ import okio.BufferedSink import okio.Okio import org.json.JSONObject import ru.gildor.coroutines.okhttp.await -import java.io.File -import java.io.FileInputStream import java.io.IOException import java.io.InputStream @@ -29,20 +28,23 @@ object MediaUploader { ): MediaUploadResponse = withContext(Dispatchers.IO) { val bitmapResult = BitmapUtils.loadBitmap(contentResolver, uri, 1000f, false) val bitmap = bitmapResult?.bitmap ?: throw IOException("bitmap is null") - uploadPhoto(bitmap) + uploadPhoto(contentResolver, bitmap) } @Suppress("BlockingMethodInNonBlockingContext") private suspend fun uploadPhoto( + contentResolver: ContentResolver, bitmap: Bitmap, ): MediaUploadResponse = withContext(Dispatchers.IO) { - val file: File = BitmapUtils.convertToJpegAndSaveToFile(bitmap, null) + val file: DocumentFile = BitmapUtils.convertToJpegAndSaveToFile(contentResolver, bitmap, null) val byteLength: Long = file.length() val options = createUploadPhotoOptions(byteLength) val headers = getUploadPhotoHeaders(options) val url = HOST + "/rupload_igphoto/" + options.name + "/" try { - FileInputStream(file).use { input -> upload(input, url, headers) } + contentResolver.openInputStream(file.uri).use { input -> + upload(input!!, url, headers) + } } finally { file.delete() } diff --git a/app/src/main/java/awais/instagrabber/utils/MediaUtils.java b/app/src/main/java/awais/instagrabber/utils/MediaUtils.java index e7ca2707..0d58a76f 100644 --- a/app/src/main/java/awais/instagrabber/utils/MediaUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/MediaUtils.java @@ -2,13 +2,17 @@ package awais.instagrabber.utils; import android.content.ContentResolver; import android.database.Cursor; +import android.media.MediaMetadataRetriever; import android.net.Uri; +import android.os.ParcelFileDescriptor; import android.provider.MediaStore; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.io.FileDescriptor; + public final class MediaUtils { private static final String TAG = MediaUtils.class.getSimpleName(); private static final String[] PROJECTION_VIDEO = { @@ -28,9 +32,7 @@ public final class MediaUtils { AppExecutors.INSTANCE.getTasksThread().submit(() -> { try (Cursor cursor = MediaStore.Video.query(contentResolver, uri, PROJECTION_VIDEO)) { if (cursor == null) { - if (listener != null) { - listener.onLoad(null); - } + listener.onLoad(null); return; } int durationColumn = cursor.getColumnIndex(MediaStore.Video.Media.DURATION); @@ -38,24 +40,16 @@ public final class MediaUtils { int heightColumn = cursor.getColumnIndex(MediaStore.Video.Media.HEIGHT); int sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE); if (cursor.moveToNext()) { - if (listener != null) { - listener.onLoad(new VideoInfo( - cursor.getLong(durationColumn), - cursor.getInt(widthColumn), - cursor.getInt(heightColumn), - cursor.getLong(sizeColumn) - )); - } + listener.onLoad(new VideoInfo( + cursor.getLong(durationColumn), + cursor.getInt(widthColumn), + cursor.getInt(heightColumn), + cursor.getLong(sizeColumn) + )); } } catch (Exception e) { Log.e(TAG, "getVideoInfo: ", e); - if (listener != null) { - listener.onFailure(e); - } - return; - } - if (listener != null) { - listener.onLoad(null); + listener.onFailure(e); } }); } @@ -64,34 +58,25 @@ public final class MediaUtils { @NonNull final Uri uri, @NonNull final OnInfoLoadListener listener) { AppExecutors.INSTANCE.getTasksThread().submit(() -> { - try (Cursor cursor = MediaStore.Video.query(contentResolver, uri, PROJECTION_AUDIO)) { - if (cursor == null) { - if (listener != null) { - listener.onLoad(null); - } + try (ParcelFileDescriptor parcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r")) { + if (parcelFileDescriptor == null) { + listener.onLoad(null); return; } - int durationColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION); - int sizeColumn = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE); - if (cursor.moveToNext()) { - if (listener != null) { - listener.onLoad(new VideoInfo( - cursor.getLong(durationColumn), - 0, - 0, - cursor.getLong(sizeColumn) - )); - } - } + final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); + final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); + mediaMetadataRetriever.setDataSource(fileDescriptor); + String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + if (TextUtils.isEmpty(duration)) duration = "0"; + listener.onLoad(new VideoInfo( + Long.parseLong(duration), + 0, + 0, + 0 + )); } catch (Exception e) { Log.e(TAG, "getVoiceInfo: ", e); - if (listener != null) { - listener.onFailure(e); - } - return; - } - if (listener != null) { - listener.onLoad(null); + listener.onFailure(e); } }); } diff --git a/app/src/main/java/awais/instagrabber/utils/PermissionUtils.java b/app/src/main/java/awais/instagrabber/utils/PermissionUtils.java index b0ec5f61..8b92fe1d 100644 --- a/app/src/main/java/awais/instagrabber/utils/PermissionUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/PermissionUtils.java @@ -13,17 +13,15 @@ import androidx.fragment.app.Fragment; import static android.Manifest.permission.CAMERA; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.RECORD_AUDIO; -import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; import static androidx.core.content.PermissionChecker.checkSelfPermission; public class PermissionUtils { - public static final String[] AUDIO_RECORD_PERMS = new String[]{WRITE_EXTERNAL_STORAGE, RECORD_AUDIO}; + public static final String[] AUDIO_RECORD_PERMS = new String[]{RECORD_AUDIO}; public static final String[] ATTACH_MEDIA_PERMS = new String[]{READ_EXTERNAL_STORAGE}; public static final String[] CAMERA_PERMS = new String[]{CAMERA}; public static boolean hasAudioRecordPerms(@NonNull final Context context) { - return checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PermissionChecker.PERMISSION_GRANTED - && checkSelfPermission(context, RECORD_AUDIO) == PermissionChecker.PERMISSION_GRANTED; + return checkSelfPermission(context, RECORD_AUDIO) == PermissionChecker.PERMISSION_GRANTED; } public static void requestAudioRecordPerms(final Fragment fragment, final int requestCode) { diff --git a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java index 74c69b9c..6bd688d2 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -11,6 +11,7 @@ import androidx.appcompat.app.AppCompatDelegate; import java.util.HashSet; import java.util.Set; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_BARINSTA_DIR_URI; import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH; import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_NUMBER; import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT; @@ -159,7 +160,7 @@ public final class SettingsHelper { CUSTOM_DATE_TIME_FORMAT, DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT, PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT, PREF_LOCATION_POSTS_LAYOUT, PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, PREF_SAVED_POSTS_LAYOUT, - STORY_SORT, PREF_EMOJI_VARIANTS, PREF_REACTIONS, PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT, PREF_TAB_ORDER}) + STORY_SORT, PREF_EMOJI_VARIANTS, PREF_REACTIONS, PREF_ENABLE_DM_AUTO_REFRESH_FREQ_UNIT, PREF_TAB_ORDER, PREF_BARINSTA_DIR_URI}) public @interface StringSettings {} @StringDef({DOWNLOAD_USER_FOLDER, DOWNLOAD_PREPEND_USER_NAME, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, MUTED_VIDEOS, diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java index b64cc0d6..b6fd06b2 100644 --- a/app/src/main/java/awais/instagrabber/utils/Utils.java +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -13,11 +13,12 @@ import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.media.MediaScannerConnection; -import android.media.MediaScannerConnection.OnScanCompletedListener; import android.net.Uri; import android.os.Build; +import android.os.Environment; +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; @@ -37,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.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; @@ -46,12 +48,13 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.Ordering; -import com.google.common.io.Files; 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.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -66,6 +69,8 @@ import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.models.Tab; import awais.instagrabber.models.enums.FavoriteType; +import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_BARINSTA_DIR_URI; + public final class Utils { private static final String TAG = "Utils"; private static final int VIDEO_CACHE_MAX_BYTES = 10 * 1024 * 1024; @@ -82,6 +87,7 @@ public final class Utils { public static String cacheDir; public static String tabOrderString; private static int defaultStatusBarColor; + private static Object[] volumes; public static int convertDpToPx(final float dp) { return Math.round((dp * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT); @@ -346,18 +352,18 @@ public final class Utils { window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); } - public static void mediaScanFile(@NonNull final Context context, - @NonNull File file, - @NonNull final OnScanCompletedListener callback) { - //noinspection UnstableApiUsage - final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(Files.getFileExtension(file.getName())); - MediaScannerConnection.scanFile( - context, - new String[]{file.getAbsolutePath()}, - new String[]{mimeType}, - callback - ); - } + // public static void mediaScanFile(@NonNull final Context context, + // @NonNull File file, + // @NonNull final OnScanCompletedListener callback) { + // //noinspection UnstableApiUsage + // final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(Files.getFileExtension(file.getName())); + // MediaScannerConnection.scanFile( + // context, + // new String[]{file.getAbsolutePath()}, + // new String[]{mimeType}, + // callback + // ); + // } public static void showKeyboard(@NonNull final View view) { final Context context = view.getContext(); @@ -524,6 +530,73 @@ public final class Utils { return tabOrderString.contains(navRootString); } + // public static void scanDocumentFile(@NonNull final Context context, + // @NonNull final DocumentFile documentFile, + // @NonNull final OnScanCompletedListener callback) { + // if (!documentFile.isFile() || !documentFile.exists()) { + // Log.d(TAG, "scanDocumentFile: " + documentFile); + // callback.onScanCompleted(null, null); + // 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); + // } + + public static File getDocumentFileRealPath(@NonNull final Context context, + @NonNull final 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 (type.equalsIgnoreCase("raw")) { + return new File(split[1]); + } else { + if (volumes == null) { + final StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + if (sm == null) return null; + final Method getVolumeListMethod = sm.getClass().getMethod("getVolumeList"); + volumes = (Object[]) getVolumeListMethod.invoke(sm); + } + if (volumes == null) return null; + for (Object volume : volumes) { + final Method getUuidMethod = volume.getClass().getMethod("getUuid"); + final String uuid = (String) getUuidMethod.invoke(volume); + + if (uuid != null && uuid.equalsIgnoreCase(type)) { + final Method getPathMethod = volume.getClass().getMethod("getPath"); + final String path = (String) getPathMethod.invoke(volume); + return new File(path, split[1]); + } + } + } + + return null; + } + + public static void setupSelectedDir(@NonNull final Context context, + @NonNull final Intent intent) throws DownloadUtils.ReselectDocumentTreeException { + final Uri dirUri = intent.getData(); + Log.d(TAG, "onActivityResult: " + dirUri); + if (dirUri == null) return; + final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + context.getContentResolver().takePersistableUriPermission(dirUri, takeFlags); + settingsHelper.putString(PREF_BARINSTA_DIR_URI, dirUri.toString()); + // re-init DownloadUtils + DownloadUtils.init(context); + } + @NonNull public static Point getNavigationBarSize(@NonNull Context context) { Point appUsableSize = getAppUsableScreenSize(context); diff --git a/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java b/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java index 83f79c7f..266d814c 100644 --- a/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java +++ b/app/src/main/java/awais/instagrabber/utils/VoiceRecorder.java @@ -1,13 +1,18 @@ package awais.instagrabber.utils; +import android.app.Application; +import android.content.ContentResolver; 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.IOException; import java.io.File; import java.time.format.DateTimeFormatter; import java.time.LocalDateTime; @@ -27,28 +32,30 @@ public class VoiceRecorder { private static final DateTimeFormatter SIMPLE_DATE_FORMAT = DateTimeFormatter.ofPattern(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 ContentResolver contentResolver) { stopped = false; + ParcelFileDescriptor parcelFileDescriptor = null; try { recorder = new MediaRecorder(); recorder.setAudioSource(MediaRecorder.AudioSource.MIC); recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); deleteTempAudioFile(); audioTempFile = getAudioRecordFile(); - recorder.setOutputFile(audioTempFile.getAbsolutePath()); + parcelFileDescriptor = contentResolver.openFileDescriptor(audioTempFile.getUri(), "rwt"); + recorder.setOutputFile(parcelFileDescriptor.getFileDescriptor()); recorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC); recorder.setAudioEncodingBitRate(AUDIO_BIT_RATE); recorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE); @@ -63,6 +70,12 @@ public class VoiceRecorder { } catch (Exception e) { Log.e(TAG, "Audio recording failed", e); deleteTempAudioFile(); + } finally { + if (parcelFileDescriptor != null) { + try { + parcelFileDescriptor.close(); + } catch (IOException ignored) {} + } } } @@ -140,9 +153,13 @@ public class VoiceRecorder { // } @NonNull - private File getAudioRecordFile() { + private DocumentFile getAudioRecordFile() { final String name = String.format("%s-%s.%s", FILE_PREFIX, LocalDateTime.now().format(SIMPLE_DATE_FORMAT), EXTENSION); - return new File(recordingsDir, name); + DocumentFile file = recordingsDir.findFile(name); + if (file == null || !file.exists()) { + file = recordingsDir.createFile(MIME_TYPE, name); + } + return file; } private void deleteTempAudioFile() { @@ -160,11 +177,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 +191,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.kt b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt index f7626af3..70407c06 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.kt @@ -1,10 +1,10 @@ package awais.instagrabber.viewmodels +import android.R.attr import android.app.Application import android.content.ContentResolver -import android.media.MediaScannerConnection import android.net.Uri -import android.util.Log +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.* import awais.instagrabber.customviews.emoji.Emoji import awais.instagrabber.managers.DirectMessagesManager @@ -23,10 +23,9 @@ import awais.instagrabber.utils.MediaUtils.OnInfoLoadListener import awais.instagrabber.utils.MediaUtils.VideoInfo import awais.instagrabber.utils.VoiceRecorder.VoiceRecorderCallback import awais.instagrabber.utils.VoiceRecorder.VoiceRecordingResult -import awais.instagrabber.utils.extensions.TAG -import java.io.File import java.util.* + class DirectThreadViewModel( application: Application, val threadId: String, @@ -37,7 +36,7 @@ class DirectThreadViewModel( // private static final String ERROR_INVALID_THREAD = "Invalid thread"; private val contentResolver: ContentResolver = application.contentResolver - private val recordingsDir: File = DirectoryUtils.getOutputMediaDirectory(application, "Recordings") + private val recordingsDir: DocumentFile? = DownloadUtils.getRecordingsDir() private var voiceRecorder: VoiceRecorder? = null private lateinit var threadManager: ThreadManager @@ -87,33 +86,24 @@ class DirectThreadViewModel( fun startRecording(): LiveData> { val data = MutableLiveData>() - voiceRecorder = VoiceRecorder(recordingsDir, object : VoiceRecorderCallback { + voiceRecorder = VoiceRecorder(recordingsDir!!, object : VoiceRecorderCallback { override fun onStart() {} override fun onComplete(result: VoiceRecordingResult) { - Log.d(TAG, "onComplete: recording complete. Scanning file...") - MediaScannerConnection.scanFile( - getApplication(), - arrayOf(result.file.absolutePath), - arrayOf(result.mimeType) - ) { _: String?, uri: Uri? -> - if (uri == null) { - val msg = "Scan failed!" - Log.e(TAG, msg) - data.postValue(error(msg, null)) - return@scanFile - } - Log.d(TAG, "onComplete: scan complete") - MediaUtils.getVoiceInfo(contentResolver, uri, object : OnInfoLoadListener { + // Log.d(TAG, "onComplete: recording complete. Scanning file..."); + MediaUtils.getVoiceInfo( + contentResolver, + result.file.uri, + object : OnInfoLoadListener { override fun onLoad(videoInfo: VideoInfo?) { if (videoInfo == null) return threadManager.sendVoice( data, - uri, + result.file.uri, result.waveform, result.samplingFreq, videoInfo.duration, - videoInfo.size, - viewModelScope, + result.file.length(), + viewModelScope ) } @@ -121,12 +111,11 @@ class DirectThreadViewModel( data.postValue(error(t.message, null)) } }) - } } override fun onCancel() {} }) - voiceRecorder?.startRecording() + voiceRecorder?.startRecording(contentResolver) return data } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectorySelectActivityViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectorySelectActivityViewModel.java new file mode 100644 index 00000000..03e0420f --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectorySelectActivityViewModel.java @@ -0,0 +1,113 @@ +package awais.instagrabber.viewmodels; + +import android.app.Application; +import android.content.Intent; +import android.content.UriPermission; +import android.net.Uri; +import android.os.Parcelable; + +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 java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.DownloadUtils; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; + +import static awais.instagrabber.fragments.settings.PreferenceKeys.FOLDER_PATH; + +public class DirectorySelectActivityViewModel extends AndroidViewModel { + private static final String TAG = DirectorySelectActivityViewModel.class.getSimpleName(); + + private final MutableLiveData message = new MutableLiveData<>(); + private final MutableLiveData prevUri = new MutableLiveData<>(); + private final MutableLiveData loading = new MutableLiveData<>(false); + private final MutableLiveData dirSuccess = new MutableLiveData<>(false); + + public DirectorySelectActivityViewModel(final Application application) { + super(application); + } + + public LiveData getMessage() { + return message; + } + + public LiveData getPrevUri() { + return prevUri; + } + + public LiveData isLoading() { + return loading; + } + + public LiveData getDirSuccess() { + return dirSuccess; + } + + public void setInitialUri(final Intent intent) { + if (intent == null) { + setMessage(null); + return; + } + final Parcelable initialUriParcelable = intent.getParcelableExtra(Constants.EXTRA_INITIAL_URI); + if (!(initialUriParcelable instanceof Uri)) { + setMessage(null); + return; + } + setMessage((Uri) initialUriParcelable); + } + + private void setMessage(@Nullable final Uri initialUri) { + if (initialUri == null) { + final String prevVersionFolderPath = Utils.settingsHelper.getString(FOLDER_PATH); + if (TextUtils.isEmpty(prevVersionFolderPath)) { + // default message + message.postValue(getApplication().getString(R.string.dir_select_default_message)); + prevUri.postValue(null); + return; + } + message.postValue(getApplication().getString(R.string.dir_select_reselect_message)); + prevUri.postValue(prevVersionFolderPath); + return; + } + final List existingPermissions = getApplication().getContentResolver().getPersistedUriPermissions(); + final boolean anyMatch = existingPermissions.stream().anyMatch(uriPermission -> uriPermission.getUri().equals(initialUri)); + final DocumentFile documentFile = DocumentFile.fromSingleUri(getApplication(), initialUri); + String path; + try { + path = URLDecoder.decode(initialUri.toString(), StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + path = initialUri.toString(); + } + if (!anyMatch) { + message.postValue(getApplication().getString(R.string.dir_select_permission_revoked_message)); + prevUri.postValue(path); + return; + } + if (documentFile == null || !documentFile.exists() || documentFile.lastModified() == 0) { + message.postValue(getApplication().getString(R.string.dir_select_folder_not_exist)); + prevUri.postValue(path); + } + } + + public void setupSelectedDir(@NonNull final Intent data) throws DownloadUtils.ReselectDocumentTreeException { + loading.postValue(true); + try { + Utils.setupSelectedDir(getApplication(), data); + message.postValue(getApplication().getString(R.string.dir_select_success_message)); + dirSuccess.postValue(true); + } finally { + loading.postValue(false); + } + } +} diff --git a/app/src/main/java/awais/instagrabber/viewmodels/ImageEditViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/ImageEditViewModel.java index 561d4fb3..ebbfc2f6 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; @@ -24,8 +25,9 @@ import awais.instagrabber.fragments.imageedit.filters.filters.Filter; import awais.instagrabber.fragments.imageedit.filters.properties.Property; import awais.instagrabber.models.SavedImageEditState; import awais.instagrabber.utils.AppExecutors; -import awais.instagrabber.utils.DirectoryUtils; +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; @@ -34,6 +36,7 @@ public class ImageEditViewModel extends AndroidViewModel { private static final String CROP = "crop"; private static final String RESULT = "result"; private static final String FILE_FORMAT = "yyyyMMddHHmmssSSS"; + private static final String MIME_TYPE = Utils.mimeTypeMap.getMimeTypeFromExtension("jpg"); private static final DateTimeFormatter SIMPLE_DATE_FORMAT = DateTimeFormatter.ofPattern(FILE_FORMAT, Locale.US); private Uri originalUri; @@ -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 = LocalDateTime.now().format(SIMPLE_DATE_FORMAT); - outputDir = DirectoryUtils.getOutputMediaDirectory(application, "Edit", sessionId); - destinationFile = new File(outputDir, RESULT + ".jpg"); - destinationUri = Uri.fromFile(destinationFile); - cropDestinationUri = Uri.fromFile(new File(outputDir, CROP + ".jpg")); + outputDir = DownloadUtils.getImageEditDir(sessionId); + 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.kt b/app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt index a749cc07..27c47c3a 100644 --- a/app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt +++ b/app/src/main/java/awais/instagrabber/workers/DownloadWorker.kt @@ -6,8 +6,8 @@ import android.content.ContentResolver import android.content.Context 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 @@ -15,7 +15,7 @@ import android.os.Looper import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.core.content.FileProvider +import androidx.documentfile.provider.DocumentFile import androidx.work.CoroutineWorker import androidx.work.Data import androidx.work.ForegroundInfo @@ -37,11 +37,11 @@ import kotlinx.coroutines.withContext 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.net.URL import java.util.* import java.util.concurrent.ExecutionException +import java.util.stream.Collectors +import kotlin.collections.Map import kotlin.math.abs class DownloadWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { @@ -55,9 +55,14 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti .build()) } val downloadRequestString: String - val requestFile = File(downloadRequestFilePath) + val requestFile = Uri.parse(downloadRequestFilePath) + val context = applicationContext + val contentResolver = context.contentResolver ?: return Result.failure(Data.Builder() + .putString("error", "contentResolver is null") + .build()) try { - downloadRequestString = requestFile.bufferedReader().use { it.readText() } + val scanner = Scanner(contentResolver.openInputStream(requestFile)) + downloadRequestString = scanner.useDelimiter("\\A").next() } catch (e: Exception) { Log.e(TAG, "doWork: ", e) return Result.failure(Data.Builder() @@ -82,7 +87,7 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti val urlToFilePathMap = downloadRequest.urlToFilePathMap download(urlToFilePathMap) Handler(Looper.getMainLooper()).postDelayed({ showSummary(urlToFilePathMap) }, 500) - val deleted = requestFile.delete() + val deleted = DocumentFile.fromSingleUri(context, requestFile)!!.delete() if (!deleted) { Log.w(TAG, "doWork: requestFile not deleted!") } @@ -94,10 +99,11 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti val entries = urlToFilePathMap.entries var count = 1 val total = urlToFilePathMap.size - for ((url, value) in entries) { + for ((url, uriString) in entries) { updateDownloadProgress(notificationId, count, total, 0f) withContext(Dispatchers.IO) { - download(notificationId, count, total, url, value) + val file = DocumentFile.fromSingleUri(applicationContext, Uri.parse(uriString)) + download(notificationId, count, total, url, file!!) } count++ } @@ -111,47 +117,49 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti position: Int, total: Int, url: String, - filePath: String, + filePath: DocumentFile, ) { - val isJpg = filePath.endsWith("jpg") + val context = applicationContext.let { it } + val contentResolver = context.contentResolver?.let { it } ?: return + val filePathType = filePath.type?.let { it } ?: return + val isJpg = filePathType.startsWith("image") // using temp file approach to remove IPTC so that download progress can be reported - val outFile = if (isJpg) DownloadUtils.getTempFile() else File(filePath) + val outFile = if (isJpg) DownloadUtils.getTempFile(null, "jpg") else filePath try { val urlConnection = URL(url).openConnection() val fileSize = if (Build.VERSION.SDK_INT >= 24) urlConnection.contentLengthLong else urlConnection.contentLength.toLong() var totalRead = 0f try { BufferedInputStream(urlConnection.getInputStream()).use { bis -> - FileOutputStream(outFile).use { fos -> + contentResolver.openOutputStream(outFile.uri).use { fos -> val buffer = ByteArray(0x2000) var count: Int while (bis.read(buffer, 0, 0x2000).also { count = it } != -1) { totalRead += count - fos.write(buffer, 0, count) + fos!!.write(buffer, 0, count) setProgressAsync(Data.Builder().putString(URL, url) .putFloat(PROGRESS, totalRead * 100f / fileSize) .build()) updateDownloadProgress(notificationId, position, total, totalRead * 100f / fileSize) } - fos.flush() + fos!!.flush() } } } catch (e: Exception) { - Log.e(TAG, "Error while writing data from url: " + url + " to file: " + outFile.absolutePath, e) + Log.e(TAG, "Error while writing data from url: " + url + " to file: " + outFile.name, e) } if (isJpg) { - val finalFile = File(filePath) try { - FileInputStream(outFile).use { fis -> - FileOutputStream(finalFile).use { fos -> + contentResolver.openInputStream(outFile.uri).use { fis -> + contentResolver.openOutputStream(filePath.uri).use { fos -> val jpegIptcRewriter = JpegIptcRewriter() jpegIptcRewriter.removeIPTC(fis, fos) } } } catch (e: Exception) { Log.e(TAG, "Error while removing iptc: url: " + url - + ", tempFile: " + outFile.absolutePath - + ", finalFile: " + finalFile.absolutePath, e) + + ", tempFile: " + outFile.name + + ", finalFile: " + filePath.name, e) } val deleted = outFile.delete() if (!deleted) { @@ -218,53 +226,90 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti return builder.build() } - private fun showSummary(urlToFilePathMap: Map?) { + private fun showSummary(urlToFilePathMap: Map) { val context = applicationContext - val filePaths = urlToFilePathMap!!.values + val filePaths = urlToFilePathMap.mapNotNull { DocumentFile.fromSingleUri(context, Uri.parse(it.value)) } val notifications: MutableList = LinkedList() val notificationIds: MutableList = LinkedList() var count = 1 - for (filePath in filePaths) { - val file = File(filePath) - context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))) - MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), null, null) - val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) + for (filePath: DocumentFile in 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); val contentResolver = context.contentResolver - val bitmap = getThumbnail(context, file, uri, contentResolver) + var bitmap: Bitmap? = null + val mimeType = filePath.type // Utils.getMimeType(uri, contentResolver); + if (!isEmpty(mimeType)) { + if (mimeType!!.startsWith("image")) { + try { + contentResolver.openInputStream(filePath.uri).use { inputStream -> + bitmap = BitmapFactory.decodeStream(inputStream) + } + } catch (e: java.lang.Exception) { + if (BuildConfig.DEBUG) Log.e(TAG, "", e) + } + } else if (mimeType.startsWith("video")) { + val retriever = MediaMetadataRetriever() + try { + try { + retriever.setDataSource(context, filePath.uri) + } catch (e: java.lang.Exception) { + // retriever.setDataSource(file.getAbsolutePath()); + Log.e(TAG, "showSummary: ", e) + } + bitmap = retriever.frameAtTime + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) try { + retriever.close() + } catch (e: java.lang.Exception) { + Log.e(TAG, "showSummary: ", e) + } + } catch (e: java.lang.Exception) { + Log.e(TAG, "", e) + } + } + } val downloadComplete = context.getString(R.string.downloader_complete) - val intent = Intent(Intent.ACTION_VIEW, uri) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK - or Intent.FLAG_FROM_BACKGROUND - or Intent.FLAG_GRANT_READ_URI_PERMISSION - or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - .putExtra(Intent.EXTRA_STREAM, uri) + val intent = Intent(Intent.ACTION_VIEW, filePath.uri) + .addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + or Intent.FLAG_FROM_BACKGROUND + or Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + .putExtra(Intent.EXTRA_STREAM, filePath.uri) val pendingIntent = PendingIntent.getActivity( context, DOWNLOAD_NOTIFICATION_INTENT_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT ) - val notificationId = notificationId + count + val notificationId: Int = notificationId + count notificationIds.add(notificationId) count++ - val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_download) - .setContentText(null) - .setContentTitle(downloadComplete) - .setWhen(System.currentTimeMillis()) - .setOnlyAlertOnce(true) - .setAutoCancel(true) - .setGroup(NOTIF_GROUP_NAME + "_" + id) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) - .setContentIntent(pendingIntent) - .addAction(R.drawable.ic_delete, - context.getString(R.string.delete), - DeleteImageIntentService.pendingIntent(context, filePath, notificationId)) + val builder: NotificationCompat.Builder = + NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_download) + .setContentText(null) + .setContentTitle(downloadComplete) + .setWhen(System.currentTimeMillis()) + .setOnlyAlertOnce(true) + .setAutoCancel(true) + .setGroup(NOTIF_GROUP_NAME + "_" + id) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setContentIntent(pendingIntent) + .addAction( + R.drawable.ic_delete, + context.getString(R.string.delete), + DeleteImageIntentService.pendingIntent(context, filePath, notificationId) + ) if (bitmap != null) { builder.setLargeIcon(bitmap) - .setStyle(NotificationCompat.BigPictureStyle() - .bigPicture(bitmap) - .bigLargeIcon(null)) + .setStyle( + NotificationCompat.BigPictureStyle() + .bigPicture(bitmap) + .bigLargeIcon(null) + ) .setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL) } notifications.add(builder) @@ -348,13 +393,15 @@ class DownloadWorker(context: Context, workerParams: WorkerParameters) : Corouti class Builder { private var urlToFilePathMap: MutableMap = mutableMapOf() - fun setUrlToFilePathMap(urlToFilePathMap: MutableMap): Builder { + fun setUrlToFilePathMap(urlToFilePathMap: MutableMap): Builder { this.urlToFilePathMap = urlToFilePathMap + .mapValues { it.value.uri.toString() } + .toMutableMap() return this } - fun addUrl(url: String, filePath: String): Builder { - urlToFilePathMap[url] = filePath + fun addUrl(url: String, filePath: DocumentFile): Builder { + urlToFilePathMap[url] = filePath.uri.toString() return this } diff --git a/app/src/main/res/layout/activity_directory_select.xml b/app/src/main/res/layout/activity_directory_select.xml new file mode 100644 index 00000000..5d5a0110 --- /dev/null +++ b/app/src/main/res/layout/activity_directory_select.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_create_backup.xml b/app/src/main/res/layout/dialog_create_backup.xml index 78df89d5..56b4a71b 100644 --- a/app/src/main/res/layout/dialog_create_backup.xml +++ b/app/src/main/res/layout/dialog_create_backup.xml @@ -69,5 +69,5 @@ android:layout_height="wrap_content" android:layout_gravity="end" android:layout_marginTop="16dp" - android:text="@string/create_backup" /> + android:text="@string/backup" /> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_restore_backup.xml b/app/src/main/res/layout/dialog_restore_backup.xml index 52686620..195c9625 100644 --- a/app/src/main/res/layout/dialog_restore_backup.xml +++ b/app/src/main/res/layout/dialog_restore_backup.xml @@ -127,7 +127,7 @@ android:layout_height="wrap_content" android:layout_gravity="end" android:layout_marginTop="16dp" - android:text="@string/restore_backup" + android:text="@string/restore" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/bottom_password_divider" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 75f446e3..cea7976b 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -501,6 +501,16 @@ If saved, all DM related features will be disabled on next launch Copy caption Copy reply + Restore + Backup + Select a folder where Barinsta can store downloads and temporary files.\n\nYou can change this later in More > Settings > Downloads. + Android has changed the way apps can access files and directories on storage. Currently Barinsta does not have permission to access the following folder: + Permissions for the previously selected folder were revoked by the system: + The previously selected folder does not exist now: + Re-select the directory or select a new directory by clicking the button below. + No folder selected! + Success! Please wait. Starting app… + Barinsta folder Top Recent Clear