From 05c0937c6fa46bcf3a8b00ed7ac45939e099de87 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Mon, 21 Sep 2020 03:52:34 +0900 Subject: [PATCH] Add Backup and restore. Update DirectoryChooser UI. The updated backup and restore is backward compatible with old backup files. Just have updated the default file name and extension for newer backups. --- .../adapters/DirectoryFilesAdapter.java | 75 +++ .../instagrabber/adapters/SimpleAdapter.java | 75 --- .../DirectMessageInboxItemViewHolder.java | 9 +- .../dialogs/CreateBackupDialogFragment.java | 169 +++++++ .../dialogs/RestoreBackupDialogFragment.java | 180 +++++++ .../fragments/FavoritesFragment.java | 4 +- .../fragments/HashTagFragment.java | 2 +- .../fragments/LocationFragment.java | 2 +- .../fragments/main/ProfileFragment.java | 2 +- .../settings/BackupPreferencesFragment.java | 107 +++++ .../settings/BasePreferencesFragment.java | 2 +- .../settings/MorePreferencesFragment.java | 9 +- .../settings/SettingsPreferencesFragment.java | 6 +- .../awais/instagrabber/utils/Constants.java | 1 + .../awais/instagrabber/utils/DataBox.java | 80 +-- .../instagrabber/utils/DirectoryChooser.java | 181 ++++--- .../instagrabber/utils/DownloadUtils.java | 4 +- .../instagrabber/utils/ExportImportUtils.java | 454 +++++++++--------- .../instagrabber/utils/PasswordUtils.java | 47 ++ .../instagrabber/utils/SettingsHelper.java | 2 +- .../java/awais/instagrabber/utils/Utils.java | 133 +---- .../viewmodels/FileListViewModel.java | 18 + app/src/main/res/drawable/ic_file_24.xml | 10 + app/src/main/res/drawable/ic_folder_24.xml | 10 + .../ic_settings_backup_restore_24.xml | 10 + .../main/res/layout/dialog_create_backup.xml | 73 +++ .../main/res/layout/dialog_import_export.xml | 230 --------- .../main/res/layout/dialog_restore_backup.xml | 143 ++++++ app/src/main/res/layout/item_dir_list.xml | 38 +- .../res/layout/layout_directory_chooser.xml | 153 ++---- .../main/res/layout/pref_custom_folder.xml | 3 +- .../main/res/navigation/more_nav_graph.xml | 7 + app/src/main/res/values/strings.xml | 22 +- app/src/main/res/values/styles.xml | 23 +- app/src/main/res/values/themes.xml | 1 + 35 files changed, 1387 insertions(+), 898 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/adapters/DirectoryFilesAdapter.java delete mode 100755 app/src/main/java/awais/instagrabber/adapters/SimpleAdapter.java create mode 100644 app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java create mode 100644 app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/settings/BackupPreferencesFragment.java create mode 100644 app/src/main/java/awais/instagrabber/utils/PasswordUtils.java create mode 100644 app/src/main/java/awais/instagrabber/viewmodels/FileListViewModel.java create mode 100644 app/src/main/res/drawable/ic_file_24.xml create mode 100644 app/src/main/res/drawable/ic_folder_24.xml create mode 100644 app/src/main/res/drawable/ic_settings_backup_restore_24.xml create mode 100644 app/src/main/res/layout/dialog_create_backup.xml delete mode 100755 app/src/main/res/layout/dialog_import_export.xml create mode 100644 app/src/main/res/layout/dialog_restore_backup.xml diff --git a/app/src/main/java/awais/instagrabber/adapters/DirectoryFilesAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DirectoryFilesAdapter.java new file mode 100644 index 00000000..13fca194 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/DirectoryFilesAdapter.java @@ -0,0 +1,75 @@ +package awais.instagrabber.adapters; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import java.io.File; + +import awais.instagrabber.R; +import awais.instagrabber.databinding.ItemDirListBinding; + +public final class DirectoryFilesAdapter extends ListAdapter { + private final OnFileClickListener onFileClickListener; + + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull final File oldItem, @NonNull final File newItem) { + return oldItem.getAbsolutePath().equals(newItem.getAbsolutePath()); + } + + @Override + public boolean areContentsTheSame(@NonNull final File oldItem, @NonNull final File newItem) { + return oldItem.getAbsolutePath().equals(newItem.getAbsolutePath()); + } + }; + + public DirectoryFilesAdapter(final OnFileClickListener onFileClickListener) { + super(DIFF_CALLBACK); + this.onFileClickListener = onFileClickListener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + final ItemDirListBinding binding = ItemDirListBinding.inflate(inflater, parent, false); + return new ViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) { + final File file = getItem(position); + holder.bind(file, onFileClickListener); + } + + public interface OnFileClickListener { + void onFileClick(File file); + } + + static final class ViewHolder extends RecyclerView.ViewHolder { + private final ItemDirListBinding binding; + + private ViewHolder(final ItemDirListBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(final File file, final OnFileClickListener onFileClickListener) { + if (file == null) return; + if (onFileClickListener != null) { + itemView.setOnClickListener(v -> onFileClickListener.onFileClick(file)); + } + binding.text.setText(file.getName()); + if (file.isDirectory()) { + binding.icon.setImageResource(R.drawable.ic_folder_24); + return; + } + binding.icon.setImageResource(R.drawable.ic_file_24); + } + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/SimpleAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SimpleAdapter.java deleted file mode 100755 index bfdbd7b5..00000000 --- a/app/src/main/java/awais/instagrabber/adapters/SimpleAdapter.java +++ /dev/null @@ -1,75 +0,0 @@ -package awais.instagrabber.adapters; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.List; - -import awais.instagrabber.R; -import awais.instagrabber.utils.DataBox; - -public final class SimpleAdapter extends RecyclerView.Adapter { - private List items; - private final LayoutInflater layoutInflater; - private final View.OnClickListener onClickListener; - private final View.OnLongClickListener longClickListener; - - public SimpleAdapter(final Context context, final List items, final View.OnClickListener onClickListener) { - this(context, items, onClickListener, null); - } - - public SimpleAdapter(final Context context, final List items, final View.OnClickListener onClickListener, - final View.OnLongClickListener longClickListener) { - this.layoutInflater = LayoutInflater.from(context); - this.items = items; - this.onClickListener = onClickListener; - this.longClickListener = longClickListener; - } - - public void setItems(final List items) { - this.items = items; - notifyDataSetChanged(); - } - - @NonNull - @Override - public SimpleViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { - return new SimpleViewHolder(layoutInflater. - inflate(R.layout.item_dir_list, parent, false), onClickListener, longClickListener); - } - - @Override - public void onBindViewHolder(@NonNull final SimpleViewHolder holder, final int position) { - final T item = items.get(position); - holder.itemView.setTag(item); - holder.text.setText(item.toString()); - if (item instanceof DataBox.CookieModel && ((DataBox.CookieModel) item).isSelected() || - item instanceof String && ((String) item).toLowerCase().endsWith(".zaai")) - holder.itemView.setBackgroundColor(0xF0_125687); - else - holder.itemView.setBackground(null); - } - - @Override - public int getItemCount() { - return items != null ? items.size() : 0; - } - - static final class SimpleViewHolder extends RecyclerView.ViewHolder { - private final TextView text; - - private SimpleViewHolder(@NonNull final View itemView, final View.OnClickListener onClickListener, - final View.OnLongClickListener longClickListener) { - super(itemView); - text = itemView.findViewById(android.R.id.text1); - itemView.setOnClickListener(onClickListener); - if (longClickListener != null) itemView.setOnLongClickListener(longClickListener); - } - } -} diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/DirectMessageInboxItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/DirectMessageInboxItemViewHolder.java index cd975fa7..95abd782 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/DirectMessageInboxItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/DirectMessageInboxItemViewHolder.java @@ -73,7 +73,14 @@ public final class DirectMessageInboxItemViewHolder extends RecyclerView.ViewHol } } binding.tvUsername.setText(model.getThreadTitle()); - final DirectItemModel lastItemModel = itemModels[itemModels.length - 1]; + final int length = itemModels.length; + DirectItemModel lastItemModel = null; + if (length != 0) { + lastItemModel = itemModels[length - 1]; + } + if (lastItemModel == null) { + return; + } final DirectItemType itemType = lastItemModel.getItemType(); // binding.notTextType.setVisibility(itemType != DirectItemType.TEXT ? View.VISIBLE : View.GONE); final Context context = itemView.getContext(); diff --git a/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java new file mode 100644 index 00000000..eb0c6a33 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/dialogs/CreateBackupDialogFragment.java @@ -0,0 +1,169 @@ +package awais.instagrabber.dialogs; + +import android.app.Dialog; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +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.util.Locale; + +import awais.instagrabber.databinding.DialogCreateBackupBinding; +import awais.instagrabber.utils.DirectoryChooser; +import awais.instagrabber.utils.ExportImportUtils; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; + +import static awais.instagrabber.utils.Constants.FOLDER_PATH; +import static awais.instagrabber.utils.DownloadUtils.PERMS; + +public class CreateBackupDialogFragment extends DialogFragment { + private static final int STORAGE_PERM_REQUEST_CODE = 8020; + + private final OnResultListener onResultListener; + private DialogCreateBackupBinding binding; + + public CreateBackupDialogFragment(final OnResultListener onResultListener) { + this.onResultListener = onResultListener; + } + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, + final ViewGroup container, + final Bundle savedInstanceState) { + binding = DialogCreateBackupBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + return dialog; + } + + @Override + public void onStart() { + super.onStart(); + final Dialog dialog = getDialog(); + if (dialog == null) return; + final Window window = dialog.getWindow(); + if (window == null) return; + final int height = ViewGroup.LayoutParams.WRAP_CONTENT; + final int width = (int) (Utils.displayMetrics.widthPixels * 0.8); + window.setLayout(width, height); + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + init(); + } + + private void init() { + binding.etPassword.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {} + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + binding.btnSaveTo.setEnabled(!TextUtils.isEmpty(s)); + } + + @Override + public void afterTextChanged(final Editable s) {} + }); + final Context context = getContext(); + if (context == null) { + return; + } + binding.cbPassword.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + if (TextUtils.isEmpty(binding.etPassword.getText())) { + binding.btnSaveTo.setEnabled(false); + } + binding.passwordField.setVisibility(View.VISIBLE); + binding.etPassword.requestFocus(); + final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) return; + imm.showSoftInput(binding.etPassword, InputMethodManager.SHOW_IMPLICIT); + return; + } + binding.btnSaveTo.setEnabled(true); + binding.passwordField.setVisibility(View.GONE); + final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) return; + 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); + } + }); + } + + @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); + } + } + + 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 File file = new File(path, String.format(Locale.ENGLISH, "barinsta_%d.backup", System.currentTimeMillis())); + 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(password, flags, file, result -> { + if (onResultListener != null) { + onResultListener.onResult(result); + } + dismiss(); + }, context); + + }); + directoryChooser.setEnterTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + directoryChooser.setExitTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); + directoryChooser.show(getChildFragmentManager(), "directory_chooser"); + } + + public interface OnResultListener { + void onResult(boolean result); + } +} diff --git a/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java new file mode 100644 index 00000000..b5005b39 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/dialogs/RestoreBackupDialogFragment.java @@ -0,0 +1,180 @@ +package awais.instagrabber.dialogs; + +import android.app.Dialog; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +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.ExportImportUtils; +import awais.instagrabber.utils.PasswordUtils.IncorrectPasswordException; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; + +import static awais.instagrabber.utils.Constants.FOLDER_PATH; +import static awais.instagrabber.utils.DownloadUtils.PERMS; + +public class RestoreBackupDialogFragment extends DialogFragment { + private static final int STORAGE_PERM_REQUEST_CODE = 8020; + + private final OnResultListener onResultListener; + + private DialogRestoreBackupBinding binding; + private File file; + private boolean isEncrypted; + + public RestoreBackupDialogFragment(final OnResultListener onResultListener) { + this.onResultListener = onResultListener; + } + + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, + final ViewGroup container, + final Bundle savedInstanceState) { + binding = DialogRestoreBackupBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + return dialog; + } + + @Override + public void onStart() { + super.onStart(); + final Dialog dialog = getDialog(); + if (dialog == null) return; + final Window window = dialog.getWindow(); + if (window == null) return; + final int height = ViewGroup.LayoutParams.WRAP_CONTENT; + final int width = (int) (Utils.displayMetrics.widthPixels * 0.8); + window.setLayout(width, height); + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + init(); + } + + @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(); + } + } + + private void init() { + final Context context = getContext(); + if (context == null) { + return; + } + binding.btnRestore.setEnabled(false); + binding.btnRestore.setOnClickListener(v -> { + int flags = 0; + if (binding.cbFavorites.isChecked()) { + flags |= ExportImportUtils.FLAG_FAVORITES; + } + if (binding.cbSettings.isChecked()) { + flags |= ExportImportUtils.FLAG_SETTINGS; + } + if (binding.cbAccounts.isChecked()) { + flags |= ExportImportUtils.FLAG_COOKIES; + } + final Editable text = binding.etPassword.getText(); + if (isEncrypted && text == null) return; + try { + ExportImportUtils.importData( + context, + flags, + file, + !isEncrypted ? null : text.toString(), + result -> { + if (onResultListener != null) { + onResultListener.onResult(result); + } + dismiss(); + } + ); + } catch (IncorrectPasswordException e) { + binding.passwordField.setError("Incorrect password"); + } + }); + binding.etPassword.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {} + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + binding.btnRestore.setEnabled(!TextUtils.isEmpty(s)); + binding.passwordField.setError(null); + } + + @Override + public void afterTextChanged(final Editable s) {} + }); + if (ContextCompat.checkSelfPermission(context, PERMS[0]) == PackageManager.PERMISSION_GRANTED) { + showChooser(); + return; + } + requestPermissions(PERMS, STORAGE_PERM_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"); + } + + public interface OnResultListener { + void onResult(boolean result); + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java index ccf66168..3d344a59 100644 --- a/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/FavoritesFragment.java @@ -157,7 +157,7 @@ public class FavoritesFragment extends Fragment { result.getSdProfilePic(), model.getDateAdded() ); - Utils.dataBox.addFavorite(updated); + Utils.dataBox.addOrUpdateFavorite(updated); updatedList.add(i, updated); } finally { try { @@ -186,7 +186,7 @@ public class FavoritesFragment extends Fragment { result.getSdProfilePic(), model.getDateAdded() ); - Utils.dataBox.addFavorite(updated); + Utils.dataBox.addOrUpdateFavorite(updated); updatedList.add(i, updated); } finally { try { diff --git a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java index 50c497ce..7d7cd552 100644 --- a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java @@ -369,7 +369,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe binding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); message = getString(R.string.removed_from_favs); } else { - Utils.dataBox.addFavorite(new DataBox.FavoriteModel( + Utils.dataBox.addOrUpdateFavorite(new DataBox.FavoriteModel( -1, hashtag.substring(1), FavoriteType.HASHTAG, diff --git a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java index cc9ca169..673e040c 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java @@ -364,7 +364,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR binding.favChip.setChipIconResource(R.drawable.ic_outline_star_plus_24); message = getString(R.string.removed_from_favs); } else { - Utils.dataBox.addFavorite(new DataBox.FavoriteModel( + Utils.dataBox.addOrUpdateFavorite(new DataBox.FavoriteModel( -1, locationId, FavoriteType.LOCATION, diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index 0e17a115..68f4b3bf 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -741,7 +741,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe profileModel.getSdProfilePic(), new Date() ); - Utils.dataBox.addFavorite(model); + Utils.dataBox.addOrUpdateFavorite(model); binding.favCb.setButtonDrawable(R.drawable.ic_star_check_24); message = getString(R.string.added_to_favs); } else { diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/BackupPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/BackupPreferencesFragment.java new file mode 100644 index 00000000..62d59258 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/settings/BackupPreferencesFragment.java @@ -0,0 +1,107 @@ +package awais.instagrabber.fragments.settings; + +import android.content.Context; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.google.android.material.snackbar.BaseTransientBottomBar; +import com.google.android.material.snackbar.Snackbar; + +import awais.instagrabber.R; +import awais.instagrabber.dialogs.CreateBackupDialogFragment; +import awais.instagrabber.dialogs.RestoreBackupDialogFragment; + +public class BackupPreferencesFragment extends BasePreferencesFragment { + + @Override + void setupPreferenceScreen(final PreferenceScreen screen) { + final Context context = getContext(); + if (context == null) { + return; + } + screen.addPreference(getCreatePreference(context)); + screen.addPreference(getRestorePreference(context)); + } + + private Preference getCreatePreference(@NonNull final Context context) { + final Preference preference = new Preference(context); + preference.setTitle(R.string.create_backup); + preference.setIconSpaceReserved(false); + preference.setOnPreferenceClickListener(preference1 -> { + final FragmentManager fragmentManager = getParentFragmentManager(); + final CreateBackupDialogFragment fragment = new CreateBackupDialogFragment(result -> { + final View view = getView(); + if (view != null) { + Snackbar.make(view, + result ? R.string.dialog_export_success + : R.string.dialog_export_failed, + BaseTransientBottomBar.LENGTH_LONG) + .setAnimationMode(BaseTransientBottomBar.ANIMATION_MODE_SLIDE) + .setAction(R.string.ok, v -> {}) + .show(); + return; + } + Toast.makeText(context, + result ? R.string.dialog_export_success + : R.string.dialog_export_failed, + Toast.LENGTH_LONG) + .show(); + }); + final FragmentTransaction ft = fragmentManager.beginTransaction(); + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + .add(fragment, "createBackup") + .commit(); + return true; + }); + return preference; + } + + private Preference getRestorePreference(@NonNull final Context context) { + final Preference preference = new Preference(context); + preference.setTitle(R.string.restore_backup); + preference.setIconSpaceReserved(false); + preference.setOnPreferenceClickListener(preference1 -> { + final FragmentManager fragmentManager = getParentFragmentManager(); + final RestoreBackupDialogFragment fragment = new RestoreBackupDialogFragment(result -> { + final View view = getView(); + if (view != null) { + Snackbar.make(view, + result ? R.string.dialog_import_success + : R.string.dialog_import_failed, + BaseTransientBottomBar.LENGTH_LONG) + .setAnimationMode(BaseTransientBottomBar.ANIMATION_MODE_SLIDE) + .setAction(R.string.ok, v -> {}) + .addCallback(new BaseTransientBottomBar.BaseCallback() { + @Override + public void onDismissed(final Snackbar transientBottomBar, final int event) { + recreateActivity(result); + } + }) + .show(); + return; + } + recreateActivity(result); + }); + final FragmentTransaction ft = fragmentManager.beginTransaction(); + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + .add(fragment, "restoreBackup") + .commit(); + return true; + }); + return preference; + } + + private void recreateActivity(final boolean result) { + if (!result) return; + final FragmentActivity activity = getActivity(); + if (activity == null) return; + activity.recreate(); + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/BasePreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/BasePreferencesFragment.java index 973ab464..caade8b7 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/BasePreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/BasePreferencesFragment.java @@ -21,7 +21,7 @@ public abstract class BasePreferencesFragment extends PreferenceFragmentCompat i @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { final PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setSharedPreferencesName("settings"); + preferenceManager.setSharedPreferencesName(Constants.SHARED_PREFERENCES_NAME); preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(this); final Context context = getContext(); if (context == null) return; diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java index 4050a963..8b12bb38 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java @@ -20,7 +20,7 @@ import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceViewHolder; -import java.util.ArrayList; +import java.util.List; import awais.instagrabber.BuildConfig; import awais.instagrabber.R; @@ -55,7 +55,7 @@ public class MorePreferencesFragment extends BasePreferencesFragment { accountCategory.setTitle(R.string.account); accountCategory.setIconSpaceReserved(false); screen.addPreference(accountCategory); - final ArrayList allCookies = Utils.dataBox.getAllCookies(); + final List allCookies = Utils.dataBox.getAllCookies(); if (isLoggedIn) { accountCategory.setSummary(R.string.account_hint); accountCategory.addPreference(getAccountSwitcherPreference(cookie)); @@ -120,6 +120,11 @@ public class MorePreferencesFragment extends BasePreferencesFragment { NavHostFragment.findNavController(this).navigate(navDirections); return true; })); + screen.addPreference(getPreference(R.string.backup_and_restore, R.drawable.ic_settings_backup_restore_24, preference -> { + final NavDirections navDirections = MorePreferencesFragmentDirections.actionMorePreferencesFragmentToBackupPreferencesFragment(); + NavHostFragment.findNavController(this).navigate(navDirections); + return true; + })); screen.addPreference(getPreference(R.string.action_about, R.drawable.ic_outline_info_24, preference1 -> { final NavDirections navDirections = MorePreferencesFragmentDirections.actionMorePreferencesFragmentToAboutFragment(); NavHostFragment.findNavController(this).navigate(navDirections); diff --git a/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java b/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java index 7e108b17..826b0151 100644 --- a/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/settings/SettingsPreferencesFragment.java @@ -180,9 +180,9 @@ public class SettingsPreferencesFragment extends BasePreferencesFragment { if (context == null) return null; return new SaveToCustomFolderPreference(context, (resultCallback) -> new DirectoryChooser() .setInitialDirectory(settingsHelper.getString(FOLDER_PATH)) - .setInteractionListener(path -> { - settingsHelper.putString(FOLDER_PATH, path); - resultCallback.onResult(path); + .setInteractionListener(file -> { + settingsHelper.putString(FOLDER_PATH, file.getAbsolutePath()); + resultCallback.onResult(file.getAbsolutePath()); }) .show(getParentFragmentManager(), null)); } diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java index 8719d711..5a80a2fe 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -82,4 +82,5 @@ public final class Constants { public static final String PREF_DARK_THEME = "dark_theme"; public static final String PREF_LIGHT_THEME = "light_theme"; public static final String DEFAULT_HASH_TAG_PIC = "https://www.instagram.com/static/images/hashtag/search-hashtag-default-avatar.png/1d8417c9a4f5.png"; + public static final String SHARED_PREFERENCES_NAME = "settings"; } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/DataBox.java b/app/src/main/java/awais/instagrabber/utils/DataBox.java index e0e8e016..73e71b04 100755 --- a/app/src/main/java/awais/instagrabber/utils/DataBox.java +++ b/app/src/main/java/awais/instagrabber/utils/DataBox.java @@ -6,6 +6,7 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; +import android.util.Pair; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -95,7 +96,7 @@ public final class DataBox extends SQLiteOpenHelper { + FAV_COL_DATE_ADDED + " INTEGER)"); // add the old favorites back for (final FavoriteModel oldFavorite : oldFavorites) { - addFavorite(db, oldFavorite); + addOrUpdateFavorite(db, oldFavorite); } } Log.i(TAG, String.format("DB update from v%d to v%d completed!", oldVersion, newVersion)); @@ -117,18 +118,10 @@ public final class DataBox extends SQLiteOpenHelper { do { try { final String queryText = cursor.getString(cursor.getColumnIndex("query_text")); - FavoriteType type = null; - String query = null; - if (queryText.startsWith("@")) { - type = FavoriteType.USER; - query = queryText.substring(1); - } else if (queryText.contains("/")) { - type = FavoriteType.LOCATION; - query = queryText.substring(0, queryText.indexOf("/")); - } else if (queryText.startsWith("#")) { - type = FavoriteType.HASHTAG; - query = queryText.substring(1); - } + final Pair favoriteTypeQueryPair = Utils.migrateOldFavQuery(queryText); + if (favoriteTypeQueryPair == null) continue; + final FavoriteType type = favoriteTypeQueryPair.first; + final String query = favoriteTypeQueryPair.second; oldModels.add(new FavoriteModel( -1, query, @@ -170,20 +163,20 @@ public final class DataBox extends SQLiteOpenHelper { return exists; } - public final void addFavorite(@NonNull final FavoriteModel model) { + public final void addOrUpdateFavorite(@NonNull final FavoriteModel model) { final String query = model.getQuery(); if (!TextUtils.isEmpty(query)) { try (final SQLiteDatabase db = getWritableDatabase()) { db.beginTransaction(); try { - addFavorite(db, model); + addOrUpdateFavorite(db, model); db.setTransactionSuccessful(); } catch (final Exception e) { if (logCollector != null) { - logCollector.appendException(e, LogCollector.LogFile.DATA_BOX_FAVORITES, "addFavorite"); + logCollector.appendException(e, LogCollector.LogFile.DATA_BOX_FAVORITES, "addOrUpdateFavorite"); } if (BuildConfig.DEBUG) { - Log.e(TAG, "", e); + Log.e(TAG, "Error adding/updating favorite", e); } } finally { db.endTransaction(); @@ -192,16 +185,22 @@ public final class DataBox extends SQLiteOpenHelper { } } - private void addFavorite(@NonNull final SQLiteDatabase db, @NonNull final FavoriteModel model) { + private void addOrUpdateFavorite(@NonNull final SQLiteDatabase db, @NonNull final FavoriteModel model) { final ContentValues values = new ContentValues(); values.put(FAV_COL_QUERY, model.getQuery()); values.put(FAV_COL_TYPE, model.getType().toString()); values.put(FAV_COL_DISPLAY_NAME, model.getDisplayName()); values.put(FAV_COL_PIC_URL, model.getPicUrl()); values.put(FAV_COL_DATE_ADDED, model.getDateAdded().getTime()); - int rows = 0; + int rows; if (model.getId() >= 1) { rows = db.update(TABLE_FAVORITES, values, FAV_COL_ID + "=?", new String[]{String.valueOf(model.getId())}); + } else { + rows = db.update(TABLE_FAVORITES, + values, + FAV_COL_QUERY + "=?" + + " AND " + FAV_COL_TYPE + "=?", + new String[]{model.getQuery(), model.getType().toString()}); } if (rows != 1) { db.insertOrThrow(TABLE_FAVORITES, null, values); @@ -304,6 +303,16 @@ public final class DataBox extends SQLiteOpenHelper { return null; } + public final void addOrUpdateUser(@NonNull final DataBox.CookieModel cookieModel) { + addOrUpdateUser( + cookieModel.getUid(), + cookieModel.getUsername(), + cookieModel.getCookie(), + cookieModel.getFullName(), + cookieModel.getProfilePic() + ); + } + public final void addOrUpdateUser(final String uid, final String username, final String cookie, @@ -368,15 +377,6 @@ public final class DataBox extends SQLiteOpenHelper { } } - public final int getCookieCount() { - int cookieCount = 0; - try (final SQLiteDatabase db = getReadableDatabase(); - final Cursor cursor = db.rawQuery("SELECT * FROM cookies", null)) { - if (cursor != null) cookieCount = cursor.getCount(); - } - return cookieCount; - } - @Nullable public final CookieModel getCookie(final String uid) { CookieModel cookie = null; @@ -404,10 +404,9 @@ public final class DataBox extends SQLiteOpenHelper { return cookie; } - @Nullable - public final ArrayList getAllCookies() { - ArrayList cookies = null; - + @NonNull + public final List getAllCookies() { + final List cookies = new ArrayList<>(); try (final SQLiteDatabase db = getReadableDatabase(); final Cursor cursor = db.rawQuery( "SELECT " @@ -419,7 +418,6 @@ public final class DataBox extends SQLiteOpenHelper { + " FROM " + TABLE_COOKIES, null) ) { if (cursor != null && cursor.moveToFirst()) { - cookies = new ArrayList<>(); do { cookies.add(new CookieModel( cursor.getString(cursor.getColumnIndex(KEY_UID)), @@ -431,7 +429,6 @@ public final class DataBox extends SQLiteOpenHelper { } while (cursor.moveToNext()); } } - return cookies; } @@ -483,6 +480,12 @@ public final class DataBox extends SQLiteOpenHelper { this.selected = selected; } + public boolean isValid() { + return !TextUtils.isEmpty(uid) + && !TextUtils.isEmpty(username) + && !TextUtils.isEmpty(cookie); + } + @Override public boolean equals(final Object o) { if (this == o) return true; @@ -501,7 +504,14 @@ public final class DataBox extends SQLiteOpenHelper { @NonNull @Override public String toString() { - return username; + return "CookieModel{" + + "uid='" + uid + '\'' + + ", username='" + username + '\'' + + ", cookie='" + cookie + '\'' + + ", fullName='" + fullName + '\'' + + ", profilePic='" + profilePic + '\'' + + ", selected=" + selected + + '}'; } } diff --git a/app/src/main/java/awais/instagrabber/utils/DirectoryChooser.java b/app/src/main/java/awais/instagrabber/utils/DirectoryChooser.java index 969029a1..d349e490 100755 --- a/app/src/main/java/awais/instagrabber/utils/DirectoryChooser.java +++ b/app/src/main/java/awais/instagrabber/utils/DirectoryChooser.java @@ -3,20 +3,26 @@ 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; import android.os.FileObserver; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; +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 androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.snackbar.BaseTransientBottomBar; +import com.google.android.material.snackbar.Snackbar; import java.io.File; import java.util.ArrayList; @@ -24,22 +30,27 @@ import java.util.Collections; import java.util.List; import awais.instagrabber.R; -import awais.instagrabber.adapters.SimpleAdapter; +import awais.instagrabber.adapters.DirectoryFilesAdapter; +import awais.instagrabber.databinding.LayoutDirectoryChooserBinding; +import awais.instagrabber.viewmodels.FileListViewModel; public final class DirectoryChooser extends DialogFragment { + private static final String TAG = "DirectoryChooser"; + public static final String KEY_CURRENT_DIRECTORY = "CURRENT_DIRECTORY"; private static final File sdcardPathFile = Environment.getExternalStorageDirectory(); private static final String sdcardPath = sdcardPathFile.getPath(); - private final List fileNames = new ArrayList<>(); + private Context context; - private View btnConfirm, btnNavUp, btnCancel; + private LayoutDirectoryChooserBinding binding; + private FileObserver fileObserver; private File selectedDir; private String initialDirectory; - private TextView tvSelectedFolder; - private FileObserver fileObserver; - private SimpleAdapter listDirectoriesAdapter; private OnFragmentInteractionListener interactionListener; - private boolean showZaAiConfigFiles = false; + private boolean showBackupFiles = false; + private View.OnClickListener navigationOnClickListener; + private FileListViewModel fileListViewModel; + private OnCancelListener onCancelListener; public DirectoryChooser() { super(); @@ -51,8 +62,8 @@ public final class DirectoryChooser extends DialogFragment { return this; } - public DirectoryChooser setShowZaAiConfigFiles(final boolean showZaAiConfigFiles) { - this.showZaAiConfigFiles = showZaAiConfigFiles; + public DirectoryChooser setShowBackupFiles(final boolean showBackupFiles) { + this.showBackupFiles = showBackupFiles; return this; } @@ -74,60 +85,71 @@ public final class DirectoryChooser extends DialogFragment { @NonNull @Override public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + binding = LayoutDirectoryChooserBinding.inflate(inflater, container, false); + init(container); + return binding.getRoot(); + } + + private void init(final ViewGroup container) { Context context = this.context; if (context == null) context = getContext(); if (context == null) context = getActivity(); - if (context == null) context = inflater.getContext(); - - final View view = inflater.inflate(R.layout.layout_directory_chooser, container, false); - - btnNavUp = view.findViewById(R.id.btnNavUp); - btnCancel = view.findViewById(R.id.btnCancel); - btnConfirm = view.findViewById(R.id.btnConfirm); - tvSelectedFolder = view.findViewById(R.id.txtvSelectedFolder); - + 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(); + } final View.OnClickListener clickListener = v -> { - final Object tag; - if (v instanceof TextView && (tag = v.getTag()) instanceof CharSequence) { - final File file = new File(selectedDir, tag.toString()); - if (file.isDirectory()) - changeDirectory(file); - else if (showZaAiConfigFiles && file.isFile()) { - if (interactionListener != null && file.canRead()) - interactionListener.onSelectDirectory(file.getAbsolutePath()); - dismiss(); - } - - } else if (v == btnNavUp) { - final File parent; - if (selectedDir != null && (parent = selectedDir.getParentFile()) != null) - changeDirectory(parent); - - } else if (v == btnConfirm) { + if (v == binding.btnConfirm) { if (interactionListener != null && isValidFile(selectedDir)) - interactionListener.onSelectDirectory(selectedDir.getAbsolutePath()); + interactionListener.onSelectDirectory(selectedDir); dismiss(); - } else if (v == btnCancel) { + } else if (v == binding.btnCancel) { + if (onCancelListener != null) { + onCancelListener.onCancel(); + } dismiss(); } }; - btnNavUp.setOnClickListener(clickListener); - btnCancel.setOnClickListener(clickListener); - btnConfirm.setOnClickListener(clickListener); - - listDirectoriesAdapter = new SimpleAdapter<>(context, fileNames, clickListener); - - final RecyclerView directoriesList = view.findViewById(R.id.directoryList); - directoriesList.setLayoutManager(new LinearLayoutManager(context)); - directoriesList.setAdapter(listDirectoriesAdapter); - + navigationOnClickListener = v -> { + final File parent; + if (selectedDir != null && (parent = selectedDir.getParentFile()) != null) { + changeDirectory(parent); + } + }; + binding.toolbar.setNavigationOnClickListener(navigationOnClickListener); + binding.toolbar.setSubtitle(showBackupFiles ? R.string.select_backup_file : R.string.select_folder); + binding.btnCancel.setOnClickListener(clickListener); + // no need to show confirm for file picker + binding.btnConfirm.setVisibility(showBackupFiles ? View.GONE : View.VISIBLE); + if (!showBackupFiles) { + binding.btnConfirm.setOnClickListener(clickListener); + } + fileListViewModel = new ViewModelProvider(this).get(FileListViewModel.class); + final DirectoryFilesAdapter listDirectoriesAdapter = new DirectoryFilesAdapter(file -> { + if (file.isDirectory()) { + changeDirectory(file); + return; + } + if (showBackupFiles && file.isFile()) { + if (interactionListener != null && file.canRead()) { + interactionListener.onSelectDirectory(file); + } + dismiss(); + } + }); + fileListViewModel.getList().observe(this, listDirectoriesAdapter::submitList); + binding.directoryList.setLayoutManager(new LinearLayoutManager(context)); + binding.directoryList.setAdapter(listDirectoriesAdapter); final File initDir = new File(initialDirectory); final File initialDir = !TextUtils.isEmpty(initialDirectory) && isValidFile(initDir) ? initDir : Environment.getExternalStorageDirectory(); - changeDirectory(initialDir); - - return view; } @Override @@ -153,10 +175,14 @@ public final class DirectoryChooser extends DialogFragment { public void onBackPressed() { if (selectedDir != null) { final String absolutePath = selectedDir.getAbsolutePath(); - if (absolutePath.equals(sdcardPath) || absolutePath.equals(sdcardPathFile.getAbsolutePath())) + if (absolutePath.equals(sdcardPath) || absolutePath.equals(sdcardPathFile.getAbsolutePath())) { + if (onCancelListener != null) { + onCancelListener.onCancel(); + } dismiss(); - else + } else { changeDirectory(selectedDir.getParentFile()); + } } } }; @@ -189,21 +215,28 @@ public final class DirectoryChooser extends DialogFragment { private void changeDirectory(final File dir) { if (dir != null && dir.isDirectory()) { final String path = dir.getAbsolutePath(); - + binding.toolbar.setTitle(path); final File[] contents = dir.listFiles(); if (contents != null) { - fileNames.clear(); - + final List fileNames = new ArrayList<>(); for (final File f : contents) { final String name = f.getName(); - if (f.isDirectory() || showZaAiConfigFiles && f.isFile() && name.toLowerCase().endsWith(".zaai")) - fileNames.add(name); + final String nameLowerCase = name.toLowerCase(); + final boolean isBackupFile = nameLowerCase.endsWith(".zaai") || nameLowerCase.endsWith(".backup"); + if (f.isDirectory() || (showBackupFiles && f.isFile() && isBackupFile)) + fileNames.add(f); } - - Collections.sort(fileNames); + Collections.sort(fileNames, (o1, o2) -> { + if ((o1.isDirectory() && o2.isDirectory()) + || (o1.isFile() && o2.isFile())) { + return o1.getName().compareToIgnoreCase(o2.getName()); + } + if (o1.isDirectory()) return -1; + if (o2.isDirectory()) return 1; + return 0; + }); + fileListViewModel.getList().postValue(fileNames); selectedDir = dir; - tvSelectedFolder.setText(path); - listDirectoriesAdapter.notifyDataSetChanged(); fileObserver = new FileObserver(path, FileObserver.CREATE | FileObserver.DELETE | FileObserver.MOVED_FROM | FileObserver.MOVED_TO) { private final Runnable currentDirRefresher = () -> changeDirectory(selectedDir); @@ -222,15 +255,15 @@ public final class DirectoryChooser extends DialogFragment { if (selectedDir != null) { final String path = selectedDir.getAbsolutePath(); toggleUpButton(!path.equals(sdcardPathFile.getAbsolutePath()) && selectedDir != sdcardPathFile); - btnConfirm.setEnabled(isValidFile(selectedDir)); + binding.btnConfirm.setEnabled(isValidFile(selectedDir)); } } private void toggleUpButton(final boolean enable) { - if (btnNavUp != null) { - btnNavUp.setEnabled(enable); - btnNavUp.setAlpha(enable ? 1f : 0.617f); - } + binding.toolbar.setNavigationOnClickListener(enable ? navigationOnClickListener : null); + final Drawable navigationIcon = binding.toolbar.getNavigationIcon(); + if (navigationIcon == null) return; + navigationIcon.setAlpha(enable ? 255 : (int) (255 * 0.617)); } private boolean isValidFile(final File file) { @@ -242,7 +275,17 @@ public final class DirectoryChooser extends DialogFragment { return this; } + public void setOnCancelListener(final OnCancelListener onCancelListener) { + if (onCancelListener != null) { + this.onCancelListener = onCancelListener; + } + } + + public interface OnCancelListener { + void onCancel(); + } + public interface OnFragmentInteractionListener { - void onSelectDirectory(final String path); + void onSelectDirectory(final File file); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java b/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java index 4b867f87..48576a98 100644 --- a/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/DownloadUtils.java @@ -37,7 +37,9 @@ import static awais.instagrabber.utils.Constants.FOLDER_SAVE_TO; public final class DownloadUtils { public static final String[] PERMS = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; - public static void batchDownload(@NonNull final Context context, @Nullable String username, final DownloadMethod method, + public static void batchDownload(@NonNull final Context context, + @Nullable String username, + final DownloadMethod method, final List itemsToDownload) { if (Utils.settingsHelper == null) Utils.settingsHelper = new SettingsHelper(context); diff --git a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java index 33654fb4..3770bd65 100755 --- a/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ExportImportUtils.java @@ -1,35 +1,32 @@ package awais.instagrabber.utils; import android.content.Context; -import android.text.InputFilter; -import android.text.InputType; +import android.content.SharedPreferences; import android.util.Base64; import android.util.Log; +import android.util.Pair; import android.widget.Toast; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.AppCompatEditText; 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.util.ArrayList; +import java.util.Date; import java.util.Iterator; import java.util.List; - -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; +import java.util.Map; import awais.instagrabber.BuildConfig; -import awais.instagrabber.R; import awais.instagrabber.interfaces.FetchListener; +import awais.instagrabber.models.enums.FavoriteType; +import awais.instagrabber.utils.PasswordUtils.IncorrectPasswordException; import awaisomereport.LogCollector.LogFile; import static awais.instagrabber.utils.Utils.logCollector; @@ -45,90 +42,86 @@ public final class ExportImportUtils { @IntDef(value = {FLAG_COOKIES, FLAG_FAVORITES, FLAG_SETTINGS}, flag = true) @interface ExportImportFlags {} - public static void Export(@Nullable final String password, @ExportImportFlags final int flags, @NonNull final File filePath, - final FetchListener fetchListener) { - final String exportString = ExportImportUtils.getExportString(flags); - if (!TextUtils.isEmpty(exportString)) { - final boolean isPass = !TextUtils.isEmpty(password); - byte[] exportBytes = null; - - if (isPass) { - final byte[] passwordBytes = password.getBytes(); - final byte[] bytes = new byte[32]; - System.arraycopy(passwordBytes, 0, bytes, 0, Math.min(passwordBytes.length, 32)); - - try { - exportBytes = PasswordUtils.enc(exportString, bytes); - } catch (final Exception e) { - if (fetchListener != null) fetchListener.onResult(false); - if (logCollector != null) - logCollector.appendException(e, LogFile.UTILS_EXPORT, "Export::isPass"); - if (BuildConfig.DEBUG) Log.e(TAG, "", e); - } - } else { - exportBytes = Base64.encode(exportString.getBytes(), Base64.DEFAULT | Base64.NO_WRAP | Base64.NO_PADDING); + public static void exportData(@Nullable final String password, + @ExportImportFlags final int flags, + @NonNull final File filePath, + final FetchListener fetchListener, + @NonNull final Context context) { + final String exportString = getExportString(flags, context); + if (TextUtils.isEmpty(exportString)) return; + final boolean isPass = !TextUtils.isEmpty(password); + byte[] exportBytes = null; + if (isPass) { + final byte[] passwordBytes = password.getBytes(); + final byte[] bytes = new byte[32]; + System.arraycopy(passwordBytes, 0, bytes, 0, Math.min(passwordBytes.length, 32)); + try { + exportBytes = PasswordUtils.enc(exportString, bytes); + } catch (final Exception e) { + if (fetchListener != null) fetchListener.onResult(false); + if (logCollector != null) + logCollector.appendException(e, LogFile.UTILS_EXPORT, "Export::isPass"); + if (BuildConfig.DEBUG) Log.e(TAG, "", e); } - - if (exportBytes != null && exportBytes.length > 1) { - try (final FileOutputStream fos = new FileOutputStream(filePath)) { - fos.write(isPass ? 'A' : 'Z'); - fos.write(exportBytes); - if (fetchListener != null) fetchListener.onResult(true); - } catch (final Exception e) { - if (fetchListener != null) fetchListener.onResult(false); - if (logCollector != null) - logCollector.appendException(e, LogFile.UTILS_EXPORT, "Export::notPass"); - if (BuildConfig.DEBUG) Log.e(TAG, "", e); - } - } else if (fetchListener != null) fetchListener.onResult(false); + } else { + 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); + if (fetchListener != null) fetchListener.onResult(true); + } catch (final Exception e) { + if (fetchListener != null) fetchListener.onResult(false); + if (logCollector != null) + logCollector.appendException(e, LogFile.UTILS_EXPORT, "Export::notPass"); + if (BuildConfig.DEBUG) Log.e(TAG, "", e); + } + } else if (fetchListener != null) fetchListener.onResult(false); } - public static void Import(@NonNull final Context context, @ExportImportFlags final int flags, @NonNull final File filePath, - final FetchListener fetchListener) { - try (final FileInputStream fis = new FileInputStream(filePath)) { + public static void importData(@NonNull final Context context, + @ExportImportFlags final int flags, + @NonNull final File file, + final String password, + final FetchListener fetchListener) throws IncorrectPasswordException { + try (final FileInputStream fis = new FileInputStream(file)) { final int configType = fis.read(); - final StringBuilder builder = new StringBuilder(); int c; while ((c = fis.read()) != -1) { builder.append((char) c); } - if (configType == 'A') { // password - final AppCompatEditText editText = new AppCompatEditText(context); - editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(32)}); - editText.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD); - new AlertDialog.Builder(context).setView(editText).setTitle(R.string.password) - .setPositiveButton(R.string.confirm, (dialog, which) -> { - final CharSequence text = editText.getText(); - if (!TextUtils.isEmpty(text)) { - try { - final byte[] passwordBytes = text.toString().getBytes(); - final byte[] bytes = new byte[32]; - System.arraycopy(passwordBytes, 0, bytes, 0, Math.min(passwordBytes.length, 32)); - saveToSettings(new String(PasswordUtils.dec(builder.toString(), bytes)), flags, - fetchListener); - } catch (final Exception e) { - if (fetchListener != null) fetchListener.onResult(false); - if (logCollector != null) - logCollector.appendException(e, LogFile.UTILS_IMPORT, "Import::pass"); - if (BuildConfig.DEBUG) Log.e(TAG, "", e); - } - - } else - Toast.makeText(context, R.string.dialog_export_err_password_empty, Toast.LENGTH_SHORT).show(); - }).show(); - + if (TextUtils.isEmpty(password)) return; + try { + final byte[] passwordBytes = password.getBytes(); + final byte[] bytes = new byte[32]; + System.arraycopy(passwordBytes, 0, bytes, 0, Math.min(passwordBytes.length, 32)); + importJson(new String(PasswordUtils.dec(builder.toString(), bytes)), + flags, + fetchListener); + } catch (final IncorrectPasswordException e) { + throw e; + } catch (final Exception e) { + if (fetchListener != null) fetchListener.onResult(false); + if (logCollector != null) + logCollector.appendException(e, LogFile.UTILS_IMPORT, "Import::pass"); + if (BuildConfig.DEBUG) Log.e(TAG, "Error importing backup", e); + } } else if (configType == 'Z') { - saveToSettings(new String(Base64.decode(builder.toString(), Base64.DEFAULT | Base64.NO_PADDING | Base64.NO_WRAP)), - flags, fetchListener); + importJson(new String(Base64.decode(builder.toString(), Base64.DEFAULT | Base64.NO_PADDING | Base64.NO_WRAP)), + flags, + fetchListener); } else { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + Toast.makeText(context, "File is corrupted!", Toast.LENGTH_LONG).show(); if (fetchListener != null) fetchListener.onResult(false); } + } catch (IncorrectPasswordException e) { + // separately handle incorrect password + throw e; } catch (final Exception e) { if (fetchListener != null) fetchListener.onResult(false); if (logCollector != null) logCollector.appendException(e, LogFile.UTILS_IMPORT, "Import"); @@ -136,80 +129,125 @@ public final class ExportImportUtils { } } - private static void saveToSettings(final String json, @ExportImportFlags final int flags, final FetchListener fetchListener) { + private static void importJson(@NonNull final String json, + @ExportImportFlags final int flags, + final FetchListener fetchListener) { try { final JSONObject jsonObject = new JSONObject(json); - if ((flags & FLAG_SETTINGS) == FLAG_SETTINGS && jsonObject.has("settings")) { - final JSONObject objSettings = jsonObject.getJSONObject("settings"); - final Iterator keys = objSettings.keys(); - while (keys.hasNext()) { - final String key = keys.next(); - final Object val = objSettings.opt(key); - if (val instanceof String) { - settingsHelper.putString(key, (String) val); - } else if (val instanceof Integer) { - settingsHelper.putInteger(key, (int) val); - } else if (val instanceof Boolean) { - settingsHelper.putBoolean(key, (boolean) val); - } - } + importSettings(jsonObject); } - if ((flags & FLAG_COOKIES) == FLAG_COOKIES && jsonObject.has("cookies")) { - final JSONArray cookies = jsonObject.getJSONArray("cookies"); - final int cookiesLen = cookies.length(); - for (int i = 0; i < cookiesLen; ++i) { - final JSONObject cookieObject = cookies.getJSONObject(i); - // final DataBox.CookieModel cookieModel = new DataBox.CookieModel(cookieObject.getString("i"), - // cookieObject.getString("u"), - // cookieObject.getString("c"), - // fullName, - // profilePic); - // Utils.dataBox.addOrUpdateUser(cookieModel.getUid(), cookieModel.getUserInfo(), cookieModel.getCookie()); - } + importAccounts(jsonObject); } - if ((flags & FLAG_FAVORITES) == FLAG_FAVORITES && jsonObject.has("favs")) { - final JSONArray favs = jsonObject.getJSONArray("favs"); - final int favsLen = favs.length(); - for (int i = 0; i < favsLen; ++i) { - final JSONObject favsObject = favs.getJSONObject(i); - // Utils.dataBox.addFavorite(new DataBox.FavoriteModel(favsObject.getString("q"), - // favsObject.getLong("d"), - // favsObject.has("s") ? favsObject.getString("s") : favsObject.getString("q"))); - } + importFavorites(jsonObject); } - if (fetchListener != null) fetchListener.onResult(true); - } catch (final Exception e) { if (fetchListener != null) fetchListener.onResult(false); - if (logCollector != null) logCollector.appendException(e, LogFile.UTILS_IMPORT, "saveToSettings"); + if (logCollector != null) logCollector.appendException(e, LogFile.UTILS_IMPORT, "importJson"); if (BuildConfig.DEBUG) Log.e(TAG, "", e); } } + private static void importFavorites(final JSONObject jsonObject) throws JSONException { + final JSONArray favs = jsonObject.getJSONArray("favs"); + for (int i = 0; i < favs.length(); i++) { + final JSONObject favsObject = favs.getJSONObject(i); + final String queryText = favsObject.optString("q"); + if (TextUtils.isEmpty(queryText)) continue; + final Pair favoriteTypeQueryPair; + String query = null; + FavoriteType favoriteType = null; + if (queryText.contains("@") + || queryText.contains("#") + || queryText.contains("/")) { + favoriteTypeQueryPair = Utils.migrateOldFavQuery(queryText); + if (favoriteTypeQueryPair != null) { + query = favoriteTypeQueryPair.second; + favoriteType = favoriteTypeQueryPair.first; + } + } else { + query = queryText; + favoriteType = FavoriteType.valueOf(favsObject.optString("type")); + } + if (query == null || favoriteType == null) { + continue; + } + final DataBox.FavoriteModel favoriteModel = new DataBox.FavoriteModel( + -1, + query, + favoriteType, + favsObject.optString("s"), + favoriteType == FavoriteType.HASHTAG ? null + : favsObject.optString("pic_url"), + new Date(favsObject.getLong("d"))); + // Log.d(TAG, "importJson: favoriteModel: " + favoriteModel); + Utils.dataBox.addOrUpdateFavorite(favoriteModel); + } + } + + private static void importAccounts(final JSONObject jsonObject) throws JSONException { + final JSONArray cookies = jsonObject.getJSONArray("cookies"); + for (int i = 0; i < cookies.length(); i++) { + final JSONObject cookieObject = cookies.getJSONObject(i); + final DataBox.CookieModel cookieModel = new DataBox.CookieModel( + cookieObject.optString("i"), + cookieObject.optString("u"), + cookieObject.optString("c"), + cookieObject.optString("full_name"), + cookieObject.optString("profile_pic") + ); + if (!cookieModel.isValid()) continue; + // Log.d(TAG, "importJson: cookieModel: " + cookieModel); + Utils.dataBox.addOrUpdateUser(cookieModel); + } + } + + private static void importSettings(final JSONObject jsonObject) throws JSONException { + final JSONObject objSettings = jsonObject.getJSONObject("settings"); + final Iterator keys = objSettings.keys(); + while (keys.hasNext()) { + final String key = keys.next(); + final Object val = objSettings.opt(key); + // Log.d(TAG, "importJson: key: " + key + ", val: " + val); + if (val instanceof String) { + settingsHelper.putString(key, (String) val); + } else if (val instanceof Integer) { + settingsHelper.putInteger(key, (int) val); + } else if (val instanceof Boolean) { + settingsHelper.putBoolean(key, (boolean) val); + } + } + } + + public static boolean isEncrypted(final File file) { + try (final FileInputStream fis = new FileInputStream(file)) { + final int configType = fis.read(); + if (configType == 'A') { + return true; + } + } catch (final Exception e) { + Log.e(TAG, "isEncrypted", e); + } + return false; + } + @Nullable - private static String getExportString(@ExportImportFlags final int flags) { + private static String getExportString(@ExportImportFlags final int flags, + @NonNull final Context context) { String result = null; try { final JSONObject jsonObject = new JSONObject(); - - String str; if ((flags & FLAG_SETTINGS) == FLAG_SETTINGS) { - str = getSettings(); - if (str != null) jsonObject.put("settings", new JSONObject(str)); + jsonObject.put("settings", getSettings(context)); } - if ((flags & FLAG_COOKIES) == FLAG_COOKIES) { - str = getCookies(); - if (str != null) jsonObject.put("cookies", new JSONArray(str)); + jsonObject.put("cookies", getCookies()); } - if ((flags & FLAG_FAVORITES) == FLAG_FAVORITES) { - str = getFavorites(); - if (str != null) jsonObject.put("favs", new JSONArray(str)); + jsonObject.put("favs", getFavorites()); } result = jsonObject.toString(); @@ -220,117 +258,73 @@ public final class ExportImportUtils { return result; } - @Nullable - private static String getSettings() { - String result = null; - - if (settingsHelper != null) { - try { - final JSONObject json = new JSONObject(); - json.put(Constants.APP_THEME, settingsHelper.getString(Constants.APP_THEME)); - json.put(Constants.APP_LANGUAGE, settingsHelper.getString(Constants.APP_LANGUAGE)); - - String str = settingsHelper.getString(Constants.FOLDER_PATH); - if (!TextUtils.isEmpty(str)) json.put(Constants.FOLDER_PATH, str); - - str = settingsHelper.getString(Constants.DATE_TIME_FORMAT); - if (!TextUtils.isEmpty(str)) json.put(Constants.DATE_TIME_FORMAT, str); - - str = settingsHelper.getString(Constants.DATE_TIME_SELECTION); - if (!TextUtils.isEmpty(str)) json.put(Constants.DATE_TIME_SELECTION, str); - - str = settingsHelper.getString(Constants.CUSTOM_DATE_TIME_FORMAT); - if (!TextUtils.isEmpty(str)) json.put(Constants.CUSTOM_DATE_TIME_FORMAT, str); - - json.put(Constants.DOWNLOAD_USER_FOLDER, settingsHelper.getBoolean(Constants.DOWNLOAD_USER_FOLDER)); - json.put(Constants.MUTED_VIDEOS, settingsHelper.getBoolean(Constants.MUTED_VIDEOS)); - json.put(Constants.AUTOPLAY_VIDEOS, settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS)); - json.put(Constants.AUTOLOAD_POSTS, settingsHelper.getBoolean(Constants.AUTOLOAD_POSTS)); - json.put(Constants.FOLDER_SAVE_TO, settingsHelper.getBoolean(Constants.FOLDER_SAVE_TO)); - - result = json.toString(); - } catch (final Exception e) { - result = null; - if (logCollector != null) logCollector.appendException(e, LogFile.UTILS_EXPORT, "getSettings"); - if (BuildConfig.DEBUG) Log.e(TAG, "", e); - } + @NonNull + private static JSONObject getSettings(@NonNull final Context context) { + final SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + final Map allPrefs = sharedPreferences.getAll(); + if (allPrefs == null) { + return new JSONObject(); } - - return result; + try { + final JSONObject jsonObject = new JSONObject(allPrefs); + jsonObject.remove(Constants.COOKIE); + jsonObject.remove(Constants.DEVICE_UUID); + jsonObject.remove(Constants.PREV_INSTALL_VERSION); + return jsonObject; + } catch (Exception e) { + Log.e(TAG, "Error exporting settings", e); + } + return new JSONObject(); } - @Nullable - private static String getFavorites() { - String result = null; - if (Utils.dataBox != null) { - try { - final List allFavorites = Utils.dataBox.getAllFavorites(); - final int allFavoritesSize; - if ((allFavoritesSize = allFavorites.size()) > 0) { - final JSONArray jsonArray = new JSONArray(); - for (int i = 0; i < allFavoritesSize; i++) { - final DataBox.FavoriteModel favorite = allFavorites.get(i); - final JSONObject jsonObject = new JSONObject(); - jsonObject.put("q", favorite.getQuery()); - jsonObject.put("d", favorite.getDateAdded().getTime()); - jsonObject.put("s", favorite.getDisplayName()); - jsonArray.put(jsonObject); - } - result = jsonArray.toString(); - } - } catch (final Exception e) { - result = null; - if (logCollector != null) logCollector.appendException(e, LogFile.UTILS_EXPORT, "getFavorites"); - if (BuildConfig.DEBUG) Log.e(TAG, "", e); + @NonNull + private static JSONArray getFavorites() { + if (Utils.dataBox == null) return new JSONArray(); + try { + final List allFavorites = Utils.dataBox.getAllFavorites(); + final JSONArray jsonArray = new JSONArray(); + for (final DataBox.FavoriteModel favorite : allFavorites) { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("q", favorite.getQuery()); + jsonObject.put("type", favorite.getType().toString()); + jsonObject.put("s", favorite.getDisplayName()); + jsonObject.put("pic_url", favorite.getPicUrl()); + jsonObject.put("d", favorite.getDateAdded().getTime()); + jsonArray.put(jsonObject); + } + return jsonArray; + } catch (final Exception e) { + if (logCollector != null) { + logCollector.appendException(e, LogFile.UTILS_EXPORT, "getFavorites"); + } + if (BuildConfig.DEBUG) { + Log.e(TAG, "Error exporting favorites", e); } } - return result; + return new JSONArray(); } - @Nullable - private static String getCookies() { - String result = null; - if (Utils.dataBox != null) { - try { - final ArrayList allCookies = Utils.dataBox.getAllCookies(); - final int allCookiesSize; - if (allCookies != null && (allCookiesSize = allCookies.size()) > 0) { - final JSONArray jsonArray = new JSONArray(); - for (int i = 0; i < allCookiesSize; i++) { - final DataBox.CookieModel cookieModel = allCookies.get(i); - final JSONObject jsonObject = new JSONObject(); - jsonObject.put("i", cookieModel.getUid()); - jsonObject.put("u", cookieModel.getUsername()); - jsonObject.put("c", cookieModel.getCookie()); - jsonArray.put(jsonObject); - } - result = jsonArray.toString(); - } - } catch (final Exception e) { - result = null; - if (BuildConfig.DEBUG) Log.e(TAG, "", e); + @NonNull + private static JSONArray getCookies() { + if (Utils.dataBox == null) return new JSONArray(); + try { + final List allCookies = Utils.dataBox.getAllCookies(); + final JSONArray jsonArray = new JSONArray(); + for (final DataBox.CookieModel cookie : allCookies) { + final JSONObject jsonObject = new JSONObject(); + jsonObject.put("i", cookie.getUid()); + jsonObject.put("u", cookie.getUsername()); + jsonObject.put("c", cookie.getCookie()); + jsonObject.put("full_name", cookie.getFullName()); + jsonObject.put("profile_pic", cookie.getProfilePic()); + jsonArray.put(jsonObject); + } + return jsonArray; + } catch (final Exception e) { + if (BuildConfig.DEBUG) { + Log.e(TAG, "Error exporting accounts", e); } } - return result; - } - - private final static class PasswordUtils { - private static final String cipherAlgo = "AES"; - private static final String cipherTran = "AES/CBC/PKCS5Padding"; - - private static byte[] dec(final String encrypted, final byte[] keyValue) throws Exception { - final Cipher cipher = Cipher.getInstance(cipherTran); - final SecretKeySpec secretKey = new SecretKeySpec(keyValue, cipherAlgo); - cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(new byte[16])); - return cipher.doFinal(Base64.decode(encrypted, Base64.DEFAULT | Base64.NO_PADDING | Base64.NO_WRAP)); - } - - private static byte[] enc(@NonNull final String str, final byte[] keyValue) throws Exception { - final Cipher cipher = Cipher.getInstance(cipherTran); - final SecretKeySpec secretKey = new SecretKeySpec(keyValue, cipherAlgo); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(new byte[16])); - final byte[] bytes = cipher.doFinal(str.getBytes()); - return Base64.encode(bytes, Base64.DEFAULT | Base64.NO_PADDING | Base64.NO_WRAP); - } + return new JSONArray(); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/PasswordUtils.java b/app/src/main/java/awais/instagrabber/utils/PasswordUtils.java new file mode 100644 index 00000000..2cc380bc --- /dev/null +++ b/app/src/main/java/awais/instagrabber/utils/PasswordUtils.java @@ -0,0 +1,47 @@ +package awais.instagrabber.utils; + +import android.util.Base64; + +import androidx.annotation.NonNull; + +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public final class PasswordUtils { + private static final String cipherAlgo = "AES"; + private static final String cipherTran = "AES/CBC/PKCS5Padding"; + + public static byte[] dec(final String encrypted, final byte[] keyValue) throws Exception { + try { + final Cipher cipher = Cipher.getInstance(cipherTran); + final SecretKeySpec secretKey = new SecretKeySpec(keyValue, cipherAlgo); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(new byte[16])); + return cipher.doFinal(Base64.decode(encrypted, Base64.DEFAULT | Base64.NO_PADDING | Base64.NO_WRAP)); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { + throw new IncorrectPasswordException(e); + } + } + + public static byte[] enc(@NonNull final String str, final byte[] keyValue) throws Exception { + final Cipher cipher = Cipher.getInstance(cipherTran); + final SecretKeySpec secretKey = new SecretKeySpec(keyValue, cipherAlgo); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(new byte[16])); + final byte[] bytes = cipher.doFinal(str.getBytes()); + return Base64.encode(bytes, Base64.DEFAULT | Base64.NO_PADDING | Base64.NO_WRAP); + } + + public static class IncorrectPasswordException extends Exception { + public IncorrectPasswordException(final GeneralSecurityException e) { + super(e); + } + } +} diff --git a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java index a62dc9a8..cd9aef71 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -39,7 +39,7 @@ public final class SettingsHelper { private final SharedPreferences sharedPreferences; public SettingsHelper(@NonNull final Context context) { - this.sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE); + this.sharedPreferences = context.getSharedPreferences(Constants.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); } @NonNull diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java index f665a041..6e3b9ca6 100755 --- a/app/src/main/java/awais/instagrabber/utils/Utils.java +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -1,26 +1,20 @@ package awais.instagrabber.utils; import android.app.Activity; -import android.app.AlertDialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.net.Uri; -import android.os.Build; -import android.text.Editable; import android.util.DisplayMetrics; import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; +import android.util.Pair; import android.webkit.MimeTypeMap; import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.FragmentManager; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.database.ExoDatabaseProvider; import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; @@ -37,11 +31,9 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import awais.instagrabber.R; -import awais.instagrabber.databinding.DialogImportExportBinding; +import awais.instagrabber.models.enums.FavoriteType; import awaisomereport.LogCollector; -import static awais.instagrabber.utils.Constants.FOLDER_PATH; - public final class Utils { private static final String TAG = "Utils"; private static final int VIDEO_CACHE_MAX_BYTES = 10 * 1024 * 1024; @@ -62,19 +54,6 @@ public final class Utils { return Math.round((dp * displayMetrics.densityDpi) / 160.0f); } - public static void setTooltipText(final View view, @StringRes final int tooltipTextRes) { - if (view != null && tooltipTextRes != 0 && tooltipTextRes != -1) { - final Context context = view.getContext(); - final String tooltipText = context.getResources().getString(tooltipTextRes); - - if (Build.VERSION.SDK_INT >= 26) view.setTooltipText(tooltipText); - else view.setOnLongClickListener(v -> { - Toast.makeText(context, tooltipText, Toast.LENGTH_SHORT).show(); - return true; - }); - } - } - public static void copyText(@NonNull final Context context, final CharSequence string) { if (clipboardManager == null) clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); @@ -87,100 +66,6 @@ public final class Utils { Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT).show(); } - public static void showImportExportDialog(final Context context) { - final DialogImportExportBinding importExportBinding = DialogImportExportBinding.inflate(LayoutInflater.from(context)); - - final View passwordParent = (View) importExportBinding.cbPassword.getParent(); - final View exportLoginsParent = (View) importExportBinding.cbExportLogins.getParent(); - final View exportFavoritesParent = (View) importExportBinding.cbExportFavorites.getParent(); - final View exportSettingsParent = (View) importExportBinding.cbExportSettings.getParent(); - final View importLoginsParent = (View) importExportBinding.cbImportLogins.getParent(); - final View importFavoritesParent = (View) importExportBinding.cbImportFavorites.getParent(); - final View importSettingsParent = (View) importExportBinding.cbImportSettings.getParent(); - - importExportBinding.cbPassword.setOnCheckedChangeListener((buttonView, isChecked) -> - importExportBinding.etPassword.etPassword.setEnabled(isChecked)); - - final AlertDialog[] dialog = new AlertDialog[1]; - final View.OnClickListener onClickListener = v -> { - if (v == passwordParent) importExportBinding.cbPassword.performClick(); - - else if (v == exportLoginsParent) importExportBinding.cbExportLogins.performClick(); - else if (v == exportFavoritesParent) - importExportBinding.cbExportFavorites.performClick(); - - else if (v == importLoginsParent) importExportBinding.cbImportLogins.performClick(); - else if (v == importFavoritesParent) - importExportBinding.cbImportFavorites.performClick(); - - else if (v == exportSettingsParent) importExportBinding.cbExportSettings.performClick(); - else if (v == importSettingsParent) importExportBinding.cbImportSettings.performClick(); - - else if (context instanceof AppCompatActivity) { - final FragmentManager fragmentManager = ((AppCompatActivity) context).getSupportFragmentManager(); - final String folderPath = settingsHelper.getString(FOLDER_PATH); - - if (v == importExportBinding.btnSaveTo) { - final Editable text = importExportBinding.etPassword.etPassword.getText(); - final boolean passwordChecked = importExportBinding.cbPassword.isChecked(); - if (passwordChecked && TextUtils.isEmpty(text)) - Toast.makeText(context, R.string.dialog_export_err_password_empty, Toast.LENGTH_SHORT).show(); - else { - new DirectoryChooser().setInitialDirectory(folderPath).setInteractionListener(path -> { - final File file = new File(path, "InstaGrabber_Settings_" + System.currentTimeMillis() + ".zaai"); - final String password = passwordChecked ? text.toString() : null; - int flags = 0; - if (importExportBinding.cbExportFavorites.isChecked()) - flags |= ExportImportUtils.FLAG_FAVORITES; - if (importExportBinding.cbExportSettings.isChecked()) - flags |= ExportImportUtils.FLAG_SETTINGS; - if (importExportBinding.cbExportLogins.isChecked()) - flags |= ExportImportUtils.FLAG_COOKIES; - - ExportImportUtils.Export(password, flags, file, result -> { - Toast.makeText(context, result ? R.string.dialog_export_success : R.string.dialog_export_failed, Toast.LENGTH_SHORT) - .show(); - if (dialog[0] != null && dialog[0].isShowing()) dialog[0].dismiss(); - }); - - }).show(fragmentManager, null); - } - - } else if (v == importExportBinding.btnImport) { - new DirectoryChooser().setInitialDirectory(folderPath).setShowZaAiConfigFiles(true).setInteractionListener(path -> { - int flags = 0; - if (importExportBinding.cbImportFavorites.isChecked()) - flags |= ExportImportUtils.FLAG_FAVORITES; - if (importExportBinding.cbImportSettings.isChecked()) - flags |= ExportImportUtils.FLAG_SETTINGS; - if (importExportBinding.cbImportLogins.isChecked()) - flags |= ExportImportUtils.FLAG_COOKIES; - - ExportImportUtils.Import(context, flags, new File(path), result -> { - ((AppCompatActivity) context).recreate(); - Toast.makeText(context, result ? R.string.dialog_import_success : R.string.dialog_import_failed, Toast.LENGTH_SHORT) - .show(); - if (dialog[0] != null && dialog[0].isShowing()) dialog[0].dismiss(); - }); - - }).show(fragmentManager, null); - } - } - }; - - passwordParent.setOnClickListener(onClickListener); - exportLoginsParent.setOnClickListener(onClickListener); - exportSettingsParent.setOnClickListener(onClickListener); - exportFavoritesParent.setOnClickListener(onClickListener); - importLoginsParent.setOnClickListener(onClickListener); - importSettingsParent.setOnClickListener(onClickListener); - importFavoritesParent.setOnClickListener(onClickListener); - importExportBinding.btnSaveTo.setOnClickListener(onClickListener); - importExportBinding.btnImport.setOnClickListener(onClickListener); - - dialog[0] = new AlertDialog.Builder(context).setView(importExportBinding.getRoot()).show(); - } - public static Map sign(final Map form) { final String signed = sign(new JSONObject(form).toString()); if (signed == null) { @@ -249,4 +134,16 @@ public final class Utils { } return simpleCache; } + + @Nullable + public static Pair migrateOldFavQuery(final String queryText) { + if (queryText.startsWith("@")) { + return new Pair<>(FavoriteType.USER, queryText.substring(1)); + } else if (queryText.contains("/")) { + return new Pair<>(FavoriteType.LOCATION, queryText.substring(0, queryText.indexOf("/"))); + } else if (queryText.startsWith("#")) { + return new Pair<>(FavoriteType.HASHTAG, queryText.substring(1)); + } + return null; + } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/FileListViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/FileListViewModel.java new file mode 100644 index 00000000..c8ebcd5c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/FileListViewModel.java @@ -0,0 +1,18 @@ +package awais.instagrabber.viewmodels; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import java.io.File; +import java.util.List; + +public class FileListViewModel extends ViewModel { + private MutableLiveData> list; + + public MutableLiveData> getList() { + if (list == null) { + list = new MutableLiveData<>(); + } + return list; + } +} diff --git a/app/src/main/res/drawable/ic_file_24.xml b/app/src/main/res/drawable/ic_file_24.xml new file mode 100644 index 00000000..f404fbf7 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_folder_24.xml b/app/src/main/res/drawable/ic_folder_24.xml new file mode 100644 index 00000000..dc6b0802 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_backup_restore_24.xml b/app/src/main/res/drawable/ic_settings_backup_restore_24.xml new file mode 100644 index 00000000..1772ed50 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_backup_restore_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/dialog_create_backup.xml b/app/src/main/res/layout/dialog_create_backup.xml new file mode 100644 index 00000000..78df89d5 --- /dev/null +++ b/app/src/main/res/layout/dialog_create_backup.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_import_export.xml b/app/src/main/res/layout/dialog_import_export.xml deleted file mode 100755 index 0d6910f1..00000000 --- a/app/src/main/res/layout/dialog_import_export.xml +++ /dev/null @@ -1,230 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 new file mode 100644 index 00000000..52686620 --- /dev/null +++ b/app/src/main/res/layout/dialog_restore_backup.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_dir_list.xml b/app/src/main/res/layout/item_dir_list.xml index 4c959612..32a857fb 100755 --- a/app/src/main/res/layout/item_dir_list.xml +++ b/app/src/main/res/layout/item_dir_list.xml @@ -1,15 +1,31 @@ - \ No newline at end of file + android:background="?attr/selectableItemBackground" + android:minHeight="56dp"> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_directory_chooser.xml b/app/src/main/res/layout/layout_directory_chooser.xml index 561a3920..957ceaf8 100755 --- a/app/src/main/res/layout/layout_directory_chooser.xml +++ b/app/src/main/res/layout/layout_directory_chooser.xml @@ -1,116 +1,65 @@ - - - - - - - - - - - - - + app:layout_constraintBottom_toTopOf="@id/directoryList" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - - - - - - - - + app:navigationIcon="@drawable/ic_arrow_upward_24" + tools:title="/this/that/thy" /> + - \ No newline at end of file + app:layout_constraintBottom_toTopOf="@id/bottom_horizontal_divider" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/appBarLayout" /> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pref_custom_folder.xml b/app/src/main/res/layout/pref_custom_folder.xml index b1328e49..03a6aa5c 100644 --- a/app/src/main/res/layout/pref_custom_folder.xml +++ b/app/src/main/res/layout/pref_custom_folder.xml @@ -72,7 +72,8 @@ android:layout_marginLeft="15dip" android:gravity="center_vertical" android:orientation="horizontal" - android:visibility="gone"> + android:visibility="gone" + tools:visibility="visible"> + + \ 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 bdcbf8a1..ceff402a 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,8 @@ Copied to clipboard! Report Password (Max 32 chars) + Set a password (max 32 chars) + Password OK Yes Cancel @@ -25,7 +27,7 @@ Confirm Up Don\'t Show Again - Selected folder: + Current directory Favorites Discover Comments @@ -126,16 +128,18 @@ Export Import Export Logins - Export Settings - Export Favorites - Import Settings + Accounts + Settings + Favorites + Import settings Import Logins - Import Favorites + Import accounts + Import favorites Successfully imported! Failed to import! Successfully exported! Failed to export! - Password is empty! Password cannot be empt, dumbass! + Password is empty! Refresh Get cookies Desktop Mode @@ -292,4 +296,10 @@ Locations Unknown Removed from Favourites + Backup & Restore + Create + Restore + File: + Enter password + Select a backup file (.zaai/.backup) \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d982645e..54b6c291 100755 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -57,7 +57,11 @@ + + + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index c7b949a5..e1c4087a 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -38,6 +38,7 @@ @color/white @style/Widget.BottomNavigationView.Light.White @style/Widget.MaterialComponents.Button.Light.White + @style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Light.White