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