From 7b60258959a7b1f8056891dcf014c8cd0008611f Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Thu, 8 Apr 2021 23:48:44 +0900 Subject: [PATCH] Improve folder selection UI/UX --- .../activities/DirectorySelectActivity.java | 135 ++++---- .../dialogs/ConfirmDialogFragment.java | 56 +++- .../DownloadsPreferencesFragment.java | 306 ++++++++++-------- .../instagrabber/utils/DownloadUtils.java | 16 +- .../DirectorySelectActivityViewModel.java | 109 +++++++ .../res/layout/activity_directory_select.xml | 55 +++- app/src/main/res/values/strings.xml | 8 + 7 files changed, 465 insertions(+), 220 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/viewmodels/DirectorySelectActivityViewModel.java diff --git a/app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.java b/app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.java index c45338a0..e76426c0 100644 --- a/app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/DirectorySelectActivity.java @@ -1,102 +1,64 @@ package awais.instagrabber.activities; import android.content.Intent; -import android.content.UriPermission; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Parcelable; import android.provider.DocumentsContract; import android.util.Log; +import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.documentfile.provider.DocumentFile; +import androidx.lifecycle.ViewModelProvider; -import java.io.File; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.List; +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.utils.Constants; -import awais.instagrabber.utils.Utils; +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 = 1090; + 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()); - setInitialUri(); + AppExecutors.getInstance().mainThread().execute(() -> viewModel.setInitialUri(getIntent())); } - private void setInitialUri() { - AppExecutors.getInstance().mainThread().execute(() -> { - final Intent intent = getIntent(); - if (intent == null) { - setMessage(); + 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; } - final Parcelable initialUriParcelable = intent.getParcelableExtra(Constants.EXTRA_INITIAL_URI); - if (!(initialUriParcelable instanceof Uri)) { - setMessage(); - return; - } - initialUri = (Uri) initialUriParcelable; - setMessage(); + 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 setMessage() { - if (initialUri == null) { - // default message - binding.message.setText("Select a directory which Barinsta will use for downloads and temp files"); - return; - } - - if (!initialUri.toString().startsWith("content")) { - final String message = String.format("Android has changed the way apps can access files and directories on storage.\n\n" + - "Please re-select the directory '%s' after clicking the button below", - initialUri.toString()); - binding.message.setText(message); - return; - } - - final List existingPermissions = getContentResolver().getPersistedUriPermissions(); - final boolean anyMatch = existingPermissions.stream().anyMatch(uriPermission -> uriPermission.getUri().equals(initialUri)); - if (!anyMatch) { - // permission revoked message - final String message = "Permissions for the previously selected directory '%s' were revoked by the system.\n\n" + - "Re-select the directory or select a new directory."; - final DocumentFile documentFile = DocumentFile.fromSingleUri(this, initialUri); - String path; - try { - path = URLDecoder.decode(initialUri.toString(), StandardCharsets.UTF_8.toString()); - } catch (UnsupportedEncodingException e) { - path = initialUri.toString(); - } - if (documentFile != null) { - try { - final File file = Utils.getDocumentFileRealPath(this, documentFile); - if (file != null) { - path = file.getAbsolutePath(); - } - } catch (Exception e) { - Log.e(TAG, "setMessage: ", e); - } - } - binding.message.setText(String.format(message, path)); - } } private void openDirectoryChooser() { @@ -112,17 +74,42 @@ public class DirectorySelectActivity extends BaseLanguageActivity { super.onActivityResult(requestCode, resultCode, data); if (requestCode != SELECT_DIR_REQUEST_CODE) return; if (resultCode != RESULT_OK) { - // Show error + showErrorDialog(getString(R.string.select_a_folder)); return; } if (data == null || data.getData() == null) { - // show error + showErrorDialog(getString(R.string.select_a_folder)); return; } - try { - Utils.setupSelectedDir(this, data); - } catch (Exception e) { - // show error - } + AppExecutors.getInstance().mainThread().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/dialogs/ConfirmDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/ConfirmDialogFragment.java index 5b8eb3c9..076809e3 100644 --- a/app/src/main/java/awais/instagrabber/dialogs/ConfirmDialogFragment.java +++ b/app/src/main/java/awais/instagrabber/dialogs/ConfirmDialogFragment.java @@ -9,6 +9,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; @@ -28,13 +29,37 @@ public class ConfirmDialogFragment extends DialogFragment { @StringRes final int positiveText, @StringRes final int negativeText, @StringRes final int neutralText) { + return newInstance(requestCode, title, (Integer) message, positiveText, negativeText, neutralText); + } + + @NonNull + public static ConfirmDialogFragment newInstance(final int requestCode, + @StringRes final int title, + final String message, + @StringRes final int positiveText, + @StringRes final int negativeText, + @StringRes final int neutralText) { + return newInstance(requestCode, title, (Object) message, positiveText, negativeText, neutralText); + } + + @NonNull + private static ConfirmDialogFragment newInstance(final int requestCode, + @StringRes final int title, + final Object message, + @StringRes final int positiveText, + @StringRes final int negativeText, + @StringRes final int neutralText) { Bundle args = new Bundle(); args.putInt("requestCode", requestCode); if (title != 0) { args.putInt("title", title); } - if (message != 0) { - args.putInt("message", message); + if (message != null) { + if (message instanceof Integer) { + args.putInt("message", (int) message); + } else if (message instanceof String) { + args.putString("message", (String) message); + } } if (positiveText != 0) { args.putInt("positive", positiveText); @@ -48,6 +73,7 @@ public class ConfirmDialogFragment extends DialogFragment { ConfirmDialogFragment fragment = new ConfirmDialogFragment(); fragment.setArguments(args); return fragment; + } public ConfirmDialogFragment() {} @@ -55,11 +81,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 @@ -67,7 +98,7 @@ public class ConfirmDialogFragment extends DialogFragment { public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { final Bundle arguments = getArguments(); int title = 0; - int message = 0; + String message = null; int neutralButtonText = 0; int negativeButtonText = 0; @@ -75,7 +106,7 @@ public class ConfirmDialogFragment extends DialogFragment { final int requestCode; if (arguments != null) { title = arguments.getInt("title", 0); - message = arguments.getInt("message", 0); + message = getMessage(arguments); positiveButtonText = arguments.getInt("positive", defaultPositiveButtonText); negativeButtonText = arguments.getInt("negative", 0); neutralButtonText = arguments.getInt("neutral", 0); @@ -92,7 +123,7 @@ public class ConfirmDialogFragment extends DialogFragment { if (title != 0) { builder.setTitle(title); } - if (message != 0) { + if (message != null) { builder.setMessage(message); } if (negativeButtonText != 0) { @@ -110,6 +141,19 @@ public class ConfirmDialogFragment extends DialogFragment { return builder.create(); } + private String getMessage(@NonNull final Bundle arguments) { + String message = null; + final Object messageObject = arguments.get("message"); + if (messageObject != null) { + if (messageObject instanceof Integer) { + message = getString((int) messageObject); + } else if (messageObject instanceof String) { + message = (String) messageObject; + } + } + return message; + } + public interface ConfirmDialogFragmentCallback { void onPositiveButtonClicked(int requestCode); 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 254d3479..9313c79b 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/DownloadsPreferencesFragment.java @@ -1,108 +1,144 @@ package awais.instagrabber.fragments.settings; import android.content.Context; +import android.content.Intent; import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; import android.util.Log; -import android.view.View; import androidx.annotation.NonNull; -import androidx.appcompat.widget.AppCompatButton; -import androidx.appcompat.widget.AppCompatTextView; -import androidx.documentfile.provider.DocumentFile; +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.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 android.app.Activity.RESULT_OK; +import static awais.instagrabber.activities.DirectorySelectActivity.SELECT_DIR_REQUEST_CODE; +import static awais.instagrabber.utils.Constants.DOWNLOAD_USER_FOLDER; import static awais.instagrabber.utils.Constants.FOLDER_PATH; -import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO; import static awais.instagrabber.utils.Utils.settingsHelper; public class DownloadsPreferencesFragment extends BasePreferencesFragment { private static final String TAG = DownloadsPreferencesFragment.class.getSimpleName(); - private SaveToCustomFolderPreference.ResultCallback resultCallback; + // private SaveToCustomFolderPreference.ResultCallback resultCallback; @Override void setupPreferenceScreen(final PreferenceScreen screen) { final Context context = getContext(); if (context == null) return; screen.addPreference(getDownloadUserFolderPreference(context)); - // screen.addPreference(getSaveToCustomFolderPreference(context)); screen.addPreference(getPrependUsernameToFilenamePreference(context)); + screen.addPreference(getSaveToCustomFolderPreference(context)); } private Preference getDownloadUserFolderPreference(@NonNull final Context context) { final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); - preference.setKey(Constants.DOWNLOAD_USER_FOLDER); + preference.setKey(DOWNLOAD_USER_FOLDER); preference.setTitle(R.string.download_user_folder); preference.setIconSpaceReserved(false); return preference; } - // private Preference getSaveToCustomFolderPreference(@NonNull final Context context) { - // 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 Preference getSaveToCustomFolderPreference(@NonNull final Context context) { + final Preference preference = new Preference(context); + preference.setKey(FOLDER_PATH); + preference.setIconSpaceReserved(false); + preference.setTitle(R.string.barinsta_folder); + preference.setSummaryProvider(p -> { + final String currentValue = settingsHelper.getString(FOLDER_PATH); + 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 startDocumentSelector(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); - // } + 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; - // final Context context = getContext(); - // if (context == null) return; - // if (resultCode != RESULT_OK) { - // try { - // DownloadUtils.init(context, true); - // } catch (Exception ignored) {} - // return; - // } - // if (data == null || data.getData() == null) return; - // Utils.setupSelectedDir(context, data); - // if (resultCallback != null) { - // try { - // final DocumentFile root = DocumentFile.fromTreeUri(context, data.getData()); - // resultCallback.onResult(Utils.getDocumentFileRealPath(context, root).getAbsolutePath()); - // } catch (Exception e) { - // Log.e(TAG, "onActivityResult: ", e); - // } - // resultCallback = null; - // } - // // Log.d(TAG, "onActivityResult: " + root); - // } + @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.getInstance().mainThread().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) { final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); @@ -112,74 +148,74 @@ public class DownloadsPreferencesFragment extends BasePreferencesFragment { return preference; } - 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); - } - } + // 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/utils/DownloadUtils.java b/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java index 9b4e86aa..9ceaa508 100644 --- a/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java @@ -69,7 +69,7 @@ public final class DownloadUtils { // } final String customPath = Utils.settingsHelper.getString(FOLDER_PATH); if (TextUtils.isEmpty(customPath)) { - throw new ReselectDocumentTreeException(); + throw new ReselectDocumentTreeException("folder path is null or empty"); // root = DOWNLOADS_DIR_FILE; // DocumentFile.fromFile(DOWNLOADS_DIR_FILE); // return; } @@ -92,6 +92,10 @@ public final class DownloadUtils { throw new ReselectDocumentTreeException(uri); } root = DocumentFile.fromTreeUri(context, uri); + if (root == null || !root.exists() || root.lastModified() == 0) { + root = null; + throw new ReselectDocumentTreeException(uri); + } // Log.d(TAG, "init: " + root); // final File parent = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); // final DocumentFile documentFile = DocumentFile.fromFile(parent); @@ -622,6 +626,11 @@ public final class DownloadUtils { .enqueue(downloadWorkRequest); } + @Nullable + public static Uri getRootDirUri() { + return root != null ? root.getUri() : null; + } + public static class ReselectDocumentTreeException extends Exception { private final Uri initialUri; @@ -629,6 +638,11 @@ public final class DownloadUtils { initialUri = null; } + public ReselectDocumentTreeException(final String message) { + super(message); + initialUri = null; + } + public ReselectDocumentTreeException(final Uri initialUri) { this.initialUri = initialUri; } 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..3fc940ae --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectorySelectActivityViewModel.java @@ -0,0 +1,109 @@ +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.Utils; + +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) { + // default message + message.postValue(getApplication().getString(R.string.dir_select_default_message)); + prevUri.postValue(null); + return; + } + if (!initialUri.toString().startsWith("content")) { + message.postValue(getApplication().getString(R.string.dir_select_reselect_message)); + prevUri.postValue(initialUri.toString()); + 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/res/layout/activity_directory_select.xml b/app/src/main/res/layout/activity_directory_select.xml index 01f81a9f..5d5a0110 100644 --- a/app/src/main/res/layout/activity_directory_select.xml +++ b/app/src/main/res/layout/activity_directory_select.xml @@ -1,6 +1,7 @@ @@ -19,7 +20,55 @@ android:id="@+id/message" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginBottom="8dp" + android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" + app:layout_constraintBottom_toTopOf="@id/prev_uri" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + app:layout_goneMarginBottom="0dp" + tools:text="@string/dir_select_permission_revoked_message" + tools:visibility="visible" /> + + + + + + @@ -29,11 +78,9 @@ style="@style/Widget.MaterialComponents.Button.OutlinedButton" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="Select Directory" + android:text="@string/select_folder" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/message" - app:layout_constraintVertical_bias="1" /> + app:layout_constraintStart_toStartOf="parent" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3adc84b8..87f48b79 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -495,4 +495,12 @@ 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