Improve folder selection UI/UX

This commit is contained in:
Ammar Githam 2021-04-08 23:48:44 +09:00
parent c8704fb2dc
commit 7b60258959
7 changed files with 465 additions and 220 deletions

View File

@ -1,102 +1,64 @@
package awais.instagrabber.activities; package awais.instagrabber.activities;
import android.content.Intent; import android.content.Intent;
import android.content.UriPermission;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.provider.DocumentsContract; import android.provider.DocumentsContract;
import android.util.Log; import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile; import androidx.lifecycle.ViewModelProvider;
import java.io.File; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.PrintWriter;
import java.net.URLDecoder; import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.databinding.ActivityDirectorySelectBinding; import awais.instagrabber.databinding.ActivityDirectorySelectBinding;
import awais.instagrabber.dialogs.ConfirmDialogFragment;
import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.viewmodels.DirectorySelectActivityViewModel;
import awais.instagrabber.utils.Utils;
public class DirectorySelectActivity extends BaseLanguageActivity { public class DirectorySelectActivity extends BaseLanguageActivity {
private static final String TAG = DirectorySelectActivity.class.getSimpleName(); private static final String TAG = DirectorySelectActivity.class.getSimpleName();
public static final int SELECT_DIR_REQUEST_CODE = 0x01;
public static final int SELECT_DIR_REQUEST_CODE = 1090; private static final int ERROR_REQUEST_CODE = 0x02;
private Uri initialUri; private Uri initialUri;
private ActivityDirectorySelectBinding binding; private ActivityDirectorySelectBinding binding;
private DirectorySelectActivityViewModel viewModel;
@Override @Override
protected void onCreate(@Nullable final Bundle savedInstanceState) { protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
binding = ActivityDirectorySelectBinding.inflate(getLayoutInflater()); binding = ActivityDirectorySelectBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot()); setContentView(binding.getRoot());
viewModel = new ViewModelProvider(this).get(DirectorySelectActivityViewModel.class);
setupObservers();
binding.selectDir.setOnClickListener(v -> openDirectoryChooser()); binding.selectDir.setOnClickListener(v -> openDirectoryChooser());
setInitialUri(); AppExecutors.getInstance().mainThread().execute(() -> viewModel.setInitialUri(getIntent()));
} }
private void setInitialUri() { private void setupObservers() {
AppExecutors.getInstance().mainThread().execute(() -> { viewModel.getMessage().observe(this, message -> binding.message.setText(message));
final Intent intent = getIntent(); viewModel.getPrevUri().observe(this, prevUri -> {
if (intent == null) { if (prevUri == null) {
setMessage(); binding.prevUri.setVisibility(View.GONE);
binding.message2.setVisibility(View.GONE);
return; return;
} }
final Parcelable initialUriParcelable = intent.getParcelableExtra(Constants.EXTRA_INITIAL_URI); binding.prevUri.setText(prevUri);
if (!(initialUriParcelable instanceof Uri)) { binding.prevUri.setVisibility(View.VISIBLE);
setMessage(); binding.message2.setVisibility(View.VISIBLE);
return; });
} viewModel.getDirSuccess().observe(this, success -> binding.selectDir.setVisibility(success ? View.GONE : View.VISIBLE));
initialUri = (Uri) initialUriParcelable; viewModel.isLoading().observe(this, loading -> {
setMessage(); 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<UriPermission> 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() { private void openDirectoryChooser() {
@ -112,17 +74,42 @@ public class DirectorySelectActivity extends BaseLanguageActivity {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (requestCode != SELECT_DIR_REQUEST_CODE) return; if (requestCode != SELECT_DIR_REQUEST_CODE) return;
if (resultCode != RESULT_OK) { if (resultCode != RESULT_OK) {
// Show error showErrorDialog(getString(R.string.select_a_folder));
return; return;
} }
if (data == null || data.getData() == null) { if (data == null || data.getData() == null) {
// show error showErrorDialog(getString(R.string.select_a_folder));
return; return;
} }
try { AppExecutors.getInstance().mainThread().execute(() -> {
Utils.setupSelectedDir(this, data); try {
} catch (Exception e) { viewModel.setupSelectedDir(data);
// show error 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());
} }
} }

View File

@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@ -28,13 +29,37 @@ public class ConfirmDialogFragment extends DialogFragment {
@StringRes final int positiveText, @StringRes final int positiveText,
@StringRes final int negativeText, @StringRes final int negativeText,
@StringRes final int neutralText) { @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(); Bundle args = new Bundle();
args.putInt("requestCode", requestCode); args.putInt("requestCode", requestCode);
if (title != 0) { if (title != 0) {
args.putInt("title", title); args.putInt("title", title);
} }
if (message != 0) { if (message != null) {
args.putInt("message", message); if (message instanceof Integer) {
args.putInt("message", (int) message);
} else if (message instanceof String) {
args.putString("message", (String) message);
}
} }
if (positiveText != 0) { if (positiveText != 0) {
args.putInt("positive", positiveText); args.putInt("positive", positiveText);
@ -48,6 +73,7 @@ public class ConfirmDialogFragment extends DialogFragment {
ConfirmDialogFragment fragment = new ConfirmDialogFragment(); ConfirmDialogFragment fragment = new ConfirmDialogFragment();
fragment.setArguments(args); fragment.setArguments(args);
return fragment; return fragment;
} }
public ConfirmDialogFragment() {} public ConfirmDialogFragment() {}
@ -55,11 +81,16 @@ public class ConfirmDialogFragment extends DialogFragment {
@Override @Override
public void onAttach(@NonNull final Context context) { public void onAttach(@NonNull final Context context) {
super.onAttach(context); super.onAttach(context);
this.context = context;
final Fragment parentFragment = getParentFragment(); final Fragment parentFragment = getParentFragment();
if (parentFragment instanceof ConfirmDialogFragmentCallback) { if (parentFragment instanceof ConfirmDialogFragmentCallback) {
callback = (ConfirmDialogFragmentCallback) parentFragment; callback = (ConfirmDialogFragmentCallback) parentFragment;
return;
}
final FragmentActivity fragmentActivity = getActivity();
if (fragmentActivity instanceof ConfirmDialogFragmentCallback) {
callback = (ConfirmDialogFragmentCallback) fragmentActivity;
} }
this.context = context;
} }
@NonNull @NonNull
@ -67,7 +98,7 @@ public class ConfirmDialogFragment extends DialogFragment {
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
final Bundle arguments = getArguments(); final Bundle arguments = getArguments();
int title = 0; int title = 0;
int message = 0; String message = null;
int neutralButtonText = 0; int neutralButtonText = 0;
int negativeButtonText = 0; int negativeButtonText = 0;
@ -75,7 +106,7 @@ public class ConfirmDialogFragment extends DialogFragment {
final int requestCode; final int requestCode;
if (arguments != null) { if (arguments != null) {
title = arguments.getInt("title", 0); title = arguments.getInt("title", 0);
message = arguments.getInt("message", 0); message = getMessage(arguments);
positiveButtonText = arguments.getInt("positive", defaultPositiveButtonText); positiveButtonText = arguments.getInt("positive", defaultPositiveButtonText);
negativeButtonText = arguments.getInt("negative", 0); negativeButtonText = arguments.getInt("negative", 0);
neutralButtonText = arguments.getInt("neutral", 0); neutralButtonText = arguments.getInt("neutral", 0);
@ -92,7 +123,7 @@ public class ConfirmDialogFragment extends DialogFragment {
if (title != 0) { if (title != 0) {
builder.setTitle(title); builder.setTitle(title);
} }
if (message != 0) { if (message != null) {
builder.setMessage(message); builder.setMessage(message);
} }
if (negativeButtonText != 0) { if (negativeButtonText != 0) {
@ -110,6 +141,19 @@ public class ConfirmDialogFragment extends DialogFragment {
return builder.create(); 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 { public interface ConfirmDialogFragmentCallback {
void onPositiveButtonClicked(int requestCode); void onPositiveButtonClicked(int requestCode);

View File

@ -1,108 +1,144 @@
package awais.instagrabber.fragments.settings; package awais.instagrabber.fragments.settings;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.util.Log; import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatButton; import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.documentfile.provider.DocumentFile;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import androidx.preference.SwitchPreferenceCompat; 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.R;
import awais.instagrabber.dialogs.ConfirmDialogFragment;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; 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_PATH;
import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
public class DownloadsPreferencesFragment extends BasePreferencesFragment { public class DownloadsPreferencesFragment extends BasePreferencesFragment {
private static final String TAG = DownloadsPreferencesFragment.class.getSimpleName(); private static final String TAG = DownloadsPreferencesFragment.class.getSimpleName();
private SaveToCustomFolderPreference.ResultCallback resultCallback; // private SaveToCustomFolderPreference.ResultCallback resultCallback;
@Override @Override
void setupPreferenceScreen(final PreferenceScreen screen) { void setupPreferenceScreen(final PreferenceScreen screen) {
final Context context = getContext(); final Context context = getContext();
if (context == null) return; if (context == null) return;
screen.addPreference(getDownloadUserFolderPreference(context)); screen.addPreference(getDownloadUserFolderPreference(context));
// screen.addPreference(getSaveToCustomFolderPreference(context));
screen.addPreference(getPrependUsernameToFilenamePreference(context)); screen.addPreference(getPrependUsernameToFilenamePreference(context));
screen.addPreference(getSaveToCustomFolderPreference(context));
} }
private Preference getDownloadUserFolderPreference(@NonNull final Context context) { private Preference getDownloadUserFolderPreference(@NonNull final Context context) {
final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(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.setTitle(R.string.download_user_folder);
preference.setIconSpaceReserved(false); preference.setIconSpaceReserved(false);
return preference; return preference;
} }
// private Preference getSaveToCustomFolderPreference(@NonNull final Context context) { private Preference getSaveToCustomFolderPreference(@NonNull final Context context) {
// return new SaveToCustomFolderPreference(context, checked -> { final Preference preference = new Preference(context);
// try { preference.setKey(FOLDER_PATH);
// DownloadUtils.init(context); preference.setIconSpaceReserved(false);
// } catch (DownloadUtils.ReselectDocumentTreeException e) { preference.setTitle(R.string.barinsta_folder);
// if (!checked) return; preference.setSummaryProvider(p -> {
// startDocumentSelector(e.getInitialUri()); final String currentValue = settingsHelper.getString(FOLDER_PATH);
// } catch (Exception e) { if (TextUtils.isEmpty(currentValue)) return "";
// Log.e(TAG, "getSaveToCustomFolderPreference: ", e); String path;
// } try {
// }, (resultCallback) -> { path = URLDecoder.decode(currentValue, StandardCharsets.UTF_8.toString());
// // Choose a directory using the system's file picker. } catch (UnsupportedEncodingException e) {
// startDocumentSelector(null); path = currentValue;
// this.resultCallback = resultCallback; }
// return path;
// // new DirectoryChooser() });
// // .setInitialDirectory(settingsHelper.getString(FOLDER_PATH)) preference.setOnPreferenceClickListener(p -> {
// // .setInteractionListener(file -> { openDirectoryChooser(DownloadUtils.getRootDirUri());
// // settingsHelper.putString(FOLDER_PATH, file.getAbsolutePath()); return true;
// // resultCallback.onResult(file.getAbsolutePath()); });
// // }) return preference;
// // .show(getParentFragmentManager(), null); // 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) { private void openDirectoryChooser(final Uri initialUri) {
// final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialUri != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialUri != null) {
// intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri); intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri);
// } }
// startActivityForResult(intent, SELECT_DIR_REQUEST_CODE); startActivityForResult(intent, SELECT_DIR_REQUEST_CODE);
// } }
// @Override @Override
// public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) { public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
// if (requestCode != SELECT_DIR_REQUEST_CODE) return; if (requestCode != SELECT_DIR_REQUEST_CODE) return;
// final Context context = getContext(); if (resultCode != RESULT_OK) return;
// if (context == null) return; if (data == null || data.getData() == null) return;
// if (resultCode != RESULT_OK) { final Context context = getContext();
// try { if (context == null) return;
// DownloadUtils.init(context, true); AppExecutors.getInstance().mainThread().execute(() -> {
// } catch (Exception ignored) {} try {
// return; Utils.setupSelectedDir(context, data);
// } } catch (Exception e) {
// if (data == null || data.getData() == null) return; // Should not come to this point.
// Utils.setupSelectedDir(context, data); // If it does, we have to show this error to the user so that they can report it.
// if (resultCallback != null) { try (final StringWriter sw = new StringWriter();
// try { final PrintWriter pw = new PrintWriter(sw)) {
// final DocumentFile root = DocumentFile.fromTreeUri(context, data.getData()); e.printStackTrace(pw);
// resultCallback.onResult(Utils.getDocumentFileRealPath(context, root).getAbsolutePath()); final ConfirmDialogFragment dialogFragment = ConfirmDialogFragment.newInstance(
// } catch (Exception e) { 123,
// Log.e(TAG, "onActivityResult: ", e); R.string.error,
// } "Please report this error to the developers:\n\n" + sw.toString(),
// resultCallback = null; R.string.ok,
// } 0,
// // Log.d(TAG, "onActivityResult: " + root); 0
// } );
dialogFragment.show(getChildFragmentManager(), ConfirmDialogFragment.class.getSimpleName());
} catch (IOException ioException) {
Log.e(TAG, "onActivityResult: ", ioException);
}
}
}, 500);
}
private Preference getPrependUsernameToFilenamePreference(@NonNull final Context context) { private Preference getPrependUsernameToFilenamePreference(@NonNull final Context context) {
final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context); final SwitchPreferenceCompat preference = new SwitchPreferenceCompat(context);
@ -112,74 +148,74 @@ public class DownloadsPreferencesFragment extends BasePreferencesFragment {
return preference; return preference;
} }
public static class SaveToCustomFolderPreference extends Preference { // public static class SaveToCustomFolderPreference extends Preference {
private AppCompatTextView customPathTextView; // private AppCompatTextView customPathTextView;
private final OnSaveToChangeListener onSaveToChangeListener; // private final OnSaveToChangeListener onSaveToChangeListener;
private final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener; // private final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener;
private final String key; // private final String key;
//
public SaveToCustomFolderPreference(final Context context, // public SaveToCustomFolderPreference(final Context context,
final OnSaveToChangeListener onSaveToChangeListener, // final OnSaveToChangeListener onSaveToChangeListener,
final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener) { // final OnSelectFolderButtonClickListener onSelectFolderButtonClickListener) {
super(context); // super(context);
this.onSaveToChangeListener = onSaveToChangeListener; // this.onSaveToChangeListener = onSaveToChangeListener;
this.onSelectFolderButtonClickListener = onSelectFolderButtonClickListener; // this.onSelectFolderButtonClickListener = onSelectFolderButtonClickListener;
key = FOLDER_SAVE_TO; // key = FOLDER_SAVE_TO;
setLayoutResource(R.layout.pref_custom_folder); // setLayoutResource(R.layout.pref_custom_folder);
setKey(key); // setKey(key);
setTitle(R.string.save_to_folder); // setTitle(R.string.save_to_folder);
setIconSpaceReserved(false); // setIconSpaceReserved(false);
} // }
//
@Override // @Override
public void onBindViewHolder(final PreferenceViewHolder holder) { // public void onBindViewHolder(final PreferenceViewHolder holder) {
super.onBindViewHolder(holder); // super.onBindViewHolder(holder);
final SwitchMaterial cbSaveTo = (SwitchMaterial) holder.findViewById(R.id.cbSaveTo); // final SwitchMaterial cbSaveTo = (SwitchMaterial) holder.findViewById(R.id.cbSaveTo);
final View buttonContainer = holder.findViewById(R.id.button_container); // final View buttonContainer = holder.findViewById(R.id.button_container);
customPathTextView = (AppCompatTextView) holder.findViewById(R.id.custom_path); // customPathTextView = (AppCompatTextView) holder.findViewById(R.id.custom_path);
cbSaveTo.setOnCheckedChangeListener((buttonView, isChecked) -> { // cbSaveTo.setOnCheckedChangeListener((buttonView, isChecked) -> {
settingsHelper.putBoolean(FOLDER_SAVE_TO, isChecked); // settingsHelper.putBoolean(FOLDER_SAVE_TO, isChecked);
buttonContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE); // buttonContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE);
final Context context = getContext(); // final Context context = getContext();
String customPath = settingsHelper.getString(FOLDER_PATH); // String customPath = settingsHelper.getString(FOLDER_PATH);
if (!TextUtils.isEmpty(customPath) && customPath.startsWith("content") && context != null) { // if (!TextUtils.isEmpty(customPath) && customPath.startsWith("content") && context != null) {
final Uri uri = Uri.parse(customPath); // final Uri uri = Uri.parse(customPath);
final DocumentFile documentFile = DocumentFile.fromSingleUri(context, uri); // final DocumentFile documentFile = DocumentFile.fromSingleUri(context, uri);
try { // try {
customPath = Utils.getDocumentFileRealPath(context, documentFile).getAbsolutePath(); // customPath = Utils.getDocumentFileRealPath(context, documentFile).getAbsolutePath();
} catch (Exception e) { // } catch (Exception e) {
Log.e(TAG, "onBindViewHolder: ", e); // Log.e(TAG, "onBindViewHolder: ", e);
} // }
} // }
customPathTextView.setText(customPath); // customPathTextView.setText(customPath);
if (onSaveToChangeListener != null) { // if (onSaveToChangeListener != null) {
onSaveToChangeListener.onChange(isChecked); // onSaveToChangeListener.onChange(isChecked);
} // }
}); // });
final boolean savedToEnabled = settingsHelper.getBoolean(key); // final boolean savedToEnabled = settingsHelper.getBoolean(key);
holder.itemView.setOnClickListener(v -> cbSaveTo.toggle()); // holder.itemView.setOnClickListener(v -> cbSaveTo.toggle());
cbSaveTo.setChecked(savedToEnabled); // cbSaveTo.setChecked(savedToEnabled);
buttonContainer.setVisibility(savedToEnabled ? View.VISIBLE : View.GONE); // buttonContainer.setVisibility(savedToEnabled ? View.VISIBLE : View.GONE);
final AppCompatButton btnSaveTo = (AppCompatButton) holder.findViewById(R.id.btnSaveTo); // final AppCompatButton btnSaveTo = (AppCompatButton) holder.findViewById(R.id.btnSaveTo);
btnSaveTo.setOnClickListener(v -> { // btnSaveTo.setOnClickListener(v -> {
if (onSelectFolderButtonClickListener == null) return; // if (onSelectFolderButtonClickListener == null) return;
onSelectFolderButtonClickListener.onClick(result -> { // onSelectFolderButtonClickListener.onClick(result -> {
if (TextUtils.isEmpty(result)) return; // if (TextUtils.isEmpty(result)) return;
customPathTextView.setText(result); // customPathTextView.setText(result);
}); // });
}); // });
} // }
//
public interface ResultCallback { // public interface ResultCallback {
void onResult(String result); // void onResult(String result);
} // }
//
public interface OnSelectFolderButtonClickListener { // public interface OnSelectFolderButtonClickListener {
void onClick(ResultCallback resultCallback); // void onClick(ResultCallback resultCallback);
} // }
//
public interface OnSaveToChangeListener { // public interface OnSaveToChangeListener {
void onChange(boolean checked); // void onChange(boolean checked);
} // }
} // }
} }

View File

@ -69,7 +69,7 @@ public final class DownloadUtils {
// } // }
final String customPath = Utils.settingsHelper.getString(FOLDER_PATH); final String customPath = Utils.settingsHelper.getString(FOLDER_PATH);
if (TextUtils.isEmpty(customPath)) { 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); // root = DOWNLOADS_DIR_FILE; // DocumentFile.fromFile(DOWNLOADS_DIR_FILE);
// return; // return;
} }
@ -92,6 +92,10 @@ public final class DownloadUtils {
throw new ReselectDocumentTreeException(uri); throw new ReselectDocumentTreeException(uri);
} }
root = DocumentFile.fromTreeUri(context, 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); // Log.d(TAG, "init: " + root);
// final File parent = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); // final File parent = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
// final DocumentFile documentFile = DocumentFile.fromFile(parent); // final DocumentFile documentFile = DocumentFile.fromFile(parent);
@ -622,6 +626,11 @@ public final class DownloadUtils {
.enqueue(downloadWorkRequest); .enqueue(downloadWorkRequest);
} }
@Nullable
public static Uri getRootDirUri() {
return root != null ? root.getUri() : null;
}
public static class ReselectDocumentTreeException extends Exception { public static class ReselectDocumentTreeException extends Exception {
private final Uri initialUri; private final Uri initialUri;
@ -629,6 +638,11 @@ public final class DownloadUtils {
initialUri = null; initialUri = null;
} }
public ReselectDocumentTreeException(final String message) {
super(message);
initialUri = null;
}
public ReselectDocumentTreeException(final Uri initialUri) { public ReselectDocumentTreeException(final Uri initialUri) {
this.initialUri = initialUri; this.initialUri = initialUri;
} }

View File

@ -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<String> message = new MutableLiveData<>();
private final MutableLiveData<String> prevUri = new MutableLiveData<>();
private final MutableLiveData<Boolean> loading = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> dirSuccess = new MutableLiveData<>(false);
public DirectorySelectActivityViewModel(final Application application) {
super(application);
}
public LiveData<String> getMessage() {
return message;
}
public LiveData<String> getPrevUri() {
return prevUri;
}
public LiveData<Boolean> isLoading() {
return loading;
}
public LiveData<Boolean> 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<UriPermission> 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);
}
}
}

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="16dp"> android:padding="16dp">
@ -19,7 +20,55 @@
android:id="@+id/message" android:id="@+id/message"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" 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" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/prev_uri"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:fontFamily="monospace"
android:padding="8dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/blue_500"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/message2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_goneMarginBottom="0dp"
tools:text="content://something/something/content/content"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/message2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/dir_select_message2"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/prev_uri"
tools:visibility="visible" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/select_dir"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" /> app:layout_constraintTop_toBottomOf="@id/title" />
@ -29,11 +78,9 @@
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Select Directory" android:text="@string/select_folder"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -495,4 +495,12 @@
<string name="copy_reply">Copy reply</string> <string name="copy_reply">Copy reply</string>
<string name="restore">Restore</string> <string name="restore">Restore</string>
<string name="backup">Backup</string> <string name="backup">Backup</string>
<string name="dir_select_default_message">Select a folder where Barinsta can store downloads and temporary files.\n\nYou can change this later in More > Settings > Downloads.</string>
<string name="dir_select_reselect_message">Android has changed the way apps can access files and directories on storage. Currently Barinsta does not have permission to access the following folder:</string>
<string name="dir_select_permission_revoked_message">Permissions for the previously selected folder were revoked by the system:</string>
<string name="dir_select_folder_not_exist">The previously selected folder does not exist now:</string>
<string name="dir_select_message2">Re-select the directory or select a new directory by clicking the button below.</string>
<string name="select_a_folder">No folder selected!</string>
<string name="dir_select_success_message">Success! Please wait. Starting app…</string>
<string name="barinsta_folder">Barinsta folder</string>
</resources> </resources>