Convert AccountSwitcher dialog to a DialogFragment as the AlertDialog was causing a leak (detected by LeakCanary)

This commit is contained in:
Ammar Githam 2020-09-12 21:39:43 +09:00
parent 122d84fbf2
commit c52f35bc3e
4 changed files with 224 additions and 132 deletions

View File

@ -1,18 +1,16 @@
package awais.instagrabber.adapters;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.databinding.PrefAccountSwitcherBinding;
@ -21,42 +19,45 @@ import awais.instagrabber.utils.DataBox;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class AccountSwitcherListAdapter extends ArrayAdapter<DataBox.CookieModel> {
private static final String TAG = "AccountSwitcherListAdap";
public class AccountSwitcherAdapter extends ListAdapter<DataBox.CookieModel, AccountSwitcherAdapter.ViewHolder> {
private static final String TAG = "AccountSwitcherAdapter";
private static final DiffUtil.ItemCallback<DataBox.CookieModel> DIFF_CALLBACK = new DiffUtil.ItemCallback<DataBox.CookieModel>() {
@Override
public boolean areItemsTheSame(@NonNull final DataBox.CookieModel oldItem, @NonNull final DataBox.CookieModel newItem) {
return oldItem.getUid().equals(newItem.getUid());
}
@Override
public boolean areContentsTheSame(@NonNull final DataBox.CookieModel oldItem, @NonNull final DataBox.CookieModel newItem) {
return oldItem.getUid().equals(newItem.getUid());
}
};
private final OnAccountClickListener clickListener;
private final OnAccountLongClickListener longClickListener;
public AccountSwitcherListAdapter(@NonNull final Context context,
final int resource,
@NonNull final List<DataBox.CookieModel> allUsers,
final OnAccountClickListener clickListener,
final OnAccountLongClickListener longClickListener) {
super(context, resource, allUsers);
public AccountSwitcherAdapter(final OnAccountClickListener clickListener,
final OnAccountLongClickListener longClickListener) {
super(DIFF_CALLBACK);
this.clickListener = clickListener;
this.longClickListener = longClickListener;
}
@NonNull
@Override
public View getView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) {
public ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final PrefAccountSwitcherBinding binding = PrefAccountSwitcherBinding.inflate(layoutInflater, parent, false);
return new ViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
final DataBox.CookieModel model = getItem(position);
if (model == null) return;
final String cookie = settingsHelper.getString(Constants.COOKIE);
if (convertView == null) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final PrefAccountSwitcherBinding binding = PrefAccountSwitcherBinding.inflate(layoutInflater, parent, false);
final ViewHolder viewHolder = new ViewHolder(binding);
viewHolder.itemView.setTag(viewHolder);
if (model == null) return viewHolder.itemView;
final boolean equals = model.getCookie().equals(cookie);
viewHolder.bind(model, equals, clickListener, longClickListener);
return viewHolder.itemView;
}
final ViewHolder viewHolder = (ViewHolder) convertView.getTag();
if (model == null) return viewHolder.itemView;
final boolean equals = model.getCookie().equals(cookie);
viewHolder.bind(model, equals, clickListener, longClickListener);
return viewHolder.itemView;
final boolean isCurrent = model.getCookie().equals(cookie);
holder.bind(model, isCurrent, clickListener, longClickListener);
}
public interface OnAccountClickListener {
@ -67,12 +68,11 @@ public class AccountSwitcherListAdapter extends ArrayAdapter<DataBox.CookieModel
boolean onAccountLongClick(final DataBox.CookieModel model, final boolean isCurrent);
}
private static class ViewHolder {
private final View itemView;
public static class ViewHolder extends RecyclerView.ViewHolder {
private final PrefAccountSwitcherBinding binding;
public ViewHolder(final PrefAccountSwitcherBinding binding) {
this.itemView = binding.getRoot();
super(binding.getRoot());
this.binding = binding;
binding.arrowDown.setImageResource(R.drawable.ic_check_24);
}

View File

@ -0,0 +1,152 @@
package awais.instagrabber.dialogs;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.Collections;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.AccountSwitcherAdapter;
import awais.instagrabber.databinding.DialogAccountSwitcherBinding;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DataBox;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class AccountSwitcherDialogFragment extends DialogFragment {
private final OnAddAccountClickListener onAddAccountClickListener;
private DialogAccountSwitcherBinding binding;
private final AccountSwitcherAdapter.OnAccountClickListener accountClickListener = (model, isCurrent) -> {
if (isCurrent) {
dismiss();
return;
}
CookieUtils.setupCookies(model.getCookie());
settingsHelper.putString(Constants.COOKIE, model.getCookie());
final FragmentActivity activity = getActivity();
if (activity != null) activity.recreate();
dismiss();
};
private final AccountSwitcherAdapter.OnAccountLongClickListener accountLongClickListener = (model, isCurrent) -> {
final Context context = getContext();
if (context == null) return false;
if (isCurrent) {
new AlertDialog.Builder(context)
.setMessage(R.string.quick_access_cannot_delete_curr)
.setPositiveButton(R.string.ok, null)
.show();
return true;
}
new AlertDialog.Builder(context)
.setMessage(getString(R.string.quick_access_confirm_delete, model.getUsername()))
.setPositiveButton(R.string.yes, (dialog, which) -> {
Utils.dataBox.delUserCookie(model);
dismiss();
})
.setNegativeButton(R.string.cancel, null)
.show();
dismiss();
return true;
};
public AccountSwitcherDialogFragment(final OnAddAccountClickListener onAddAccountClickListener) {
this.onAddAccountClickListener = onAddAccountClickListener;
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState) {
binding = DialogAccountSwitcherBinding.inflate(inflater, container, false);
binding.accounts.setLayoutManager(new LinearLayoutManager(getContext()));
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
init();
}
@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);
}
private void init() {
final AccountSwitcherAdapter adapter = new AccountSwitcherAdapter(accountClickListener, accountLongClickListener);
binding.accounts.setAdapter(adapter);
final List<DataBox.CookieModel> allUsers = Utils.dataBox.getAllCookies();
if (allUsers == null) return;
final String cookie = settingsHelper.getString(Constants.COOKIE);
sortUserList(cookie, allUsers);
adapter.submitList(allUsers);
binding.addAccountBtn.setOnClickListener(v -> {
if (onAddAccountClickListener == null) return;
onAddAccountClickListener.onAddAccountClick(this);
});
}
/**
* Sort the user list by following logic:
* <ol>
* <li>Keep currently active account at top.
* <li>Check if any user does not have a full name.
* <li>If all have full names, sort by full names.
* <li>Otherwise, sort by the usernames
* </ol>
*
* @param cookie active cookie
* @param allUsers list of users
*/
private void sortUserList(final String cookie, final List<DataBox.CookieModel> allUsers) {
boolean sortByName = true;
for (final DataBox.CookieModel user : allUsers) {
if (TextUtils.isEmpty(user.getFullName())) {
sortByName = false;
break;
}
}
final boolean finalSortByName = sortByName;
Collections.sort(allUsers, (o1, o2) -> {
// keep current account at top
if (o1.getCookie().equals(cookie)) return -1;
if (finalSortByName) {
// sort by full name
return o1.getFullName().compareTo(o2.getFullName());
}
// otherwise sort by username
return o1.getUsername().compareTo(o2.getUsername());
});
}
public interface OnAddAccountClickListener {
void onAddAccountClick(final AccountSwitcherDialogFragment dialogFragment);
}
}

View File

@ -6,13 +6,13 @@ import android.content.Intent;
import android.content.res.Resources;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment;
import androidx.preference.Preference;
@ -21,15 +21,12 @@ import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.R;
import awais.instagrabber.activities.Login;
import awais.instagrabber.adapters.AccountSwitcherListAdapter;
import awais.instagrabber.adapters.AccountSwitcherListAdapter.OnAccountClickListener;
import awais.instagrabber.databinding.PrefAccountSwitcherBinding;
import awais.instagrabber.dialogs.AccountSwitcherDialogFragment;
import awais.instagrabber.repositories.responses.UserInfo;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
@ -40,14 +37,11 @@ import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.ProfileService;
import awais.instagrabber.webservices.ServiceCallback;
import static awais.instagrabber.adapters.AccountSwitcherListAdapter.OnAccountLongClickListener;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class MorePreferencesFragment extends BasePreferencesFragment {
private static final String TAG = "MorePreferencesFragment";
private AlertDialog accountSwitchDialog;
private DataBox.CookieModel tappedModel;
private ArrayAdapter<DataBox.CookieModel> adapter;
@Override
void setupPreferenceScreen(final PreferenceScreen screen) {
@ -175,102 +169,18 @@ public class MorePreferencesFragment extends BasePreferencesFragment {
}
private AccountSwitcherPreference getAccountSwitcherPreference(final String cookie) {
final List<DataBox.CookieModel> allUsers = Utils.dataBox.getAllCookies();
if (getContext() != null && allUsers != null) {
sortUserList(cookie, allUsers);
final OnAccountClickListener clickListener = (model, isCurrent) -> {
if (isCurrent) {
if (accountSwitchDialog == null) return;
accountSwitchDialog.dismiss();
return;
}
tappedModel = model;
shouldRecreate();
if (accountSwitchDialog == null) return;
accountSwitchDialog.dismiss();
};
final OnAccountLongClickListener longClickListener = (model, isCurrent) -> {
if (isCurrent) {
new AlertDialog.Builder(getContext())
.setMessage(R.string.quick_access_cannot_delete_curr)
.setPositiveButton(R.string.ok, null)
.show();
return true;
}
new AlertDialog.Builder(getContext())
.setMessage(getString(R.string.quick_access_confirm_delete, model.getUsername()))
.setPositiveButton(R.string.yes, (dialog, which) -> {
Utils.dataBox.delUserCookie(model);
adapter.clear();
final List<DataBox.CookieModel> users = Utils.dataBox.getAllCookies();
if (users == null) return;
adapter.addAll(users);
})
.setNegativeButton(R.string.cancel, null)
.show();
accountSwitchDialog.dismiss();
return true;
};
adapter = new AccountSwitcherListAdapter(
getContext(),
R.layout.pref_account_switcher,
allUsers,
clickListener,
longClickListener
);
accountSwitchDialog = new AlertDialog.Builder(getContext())
.setTitle("Accounts")
.setNeutralButton("Add account", (dialog1, which) -> startActivityForResult(
new Intent(getContext(), Login.class),
Constants.LOGIN_RESULT_CODE))
.setAdapter(adapter, null)
.create();
accountSwitchDialog.setOnDismissListener(dialog -> {
if (tappedModel == null) return;
CookieUtils.setupCookies(tappedModel.getCookie());
settingsHelper.putString(Constants.COOKIE, tappedModel.getCookie());
});
}
final AlertDialog finalDialog = accountSwitchDialog;
final Context context = getContext();
if (context == null) return null;
return new AccountSwitcherPreference(context, cookie, v -> {
if (finalDialog == null) return;
finalDialog.show();
});
return new AccountSwitcherPreference(context, cookie, v -> showAccountSwitcherDialog());
}
/**
* Sort the user list by following logic:
* <ol>
* <li>Keep currently active account at top.
* <li>Check if any user does not have a full name.
* <li>If all have full names, sort by full names.
* <li>Otherwise, sort by the usernames
* </ol>
*
* @param cookie active cookie
* @param allUsers list of users
*/
private void sortUserList(final String cookie, final List<DataBox.CookieModel> allUsers) {
boolean sortByName = true;
for (final DataBox.CookieModel user : allUsers) {
if (TextUtils.isEmpty(user.getFullName())) {
sortByName = false;
break;
}
}
final boolean finalSortByName = sortByName;
Collections.sort(allUsers, (o1, o2) -> {
// keep current account at top
if (o1.getCookie().equals(cookie)) return -1;
if (finalSortByName) {
// sort by full name
return o1.getFullName().compareTo(o2.getFullName());
}
// otherwise sort by username
return o1.getUsername().compareTo(o2.getUsername());
private void showAccountSwitcherDialog() {
final AccountSwitcherDialogFragment dialogFragment = new AccountSwitcherDialogFragment(dialog -> {
dialog.dismiss();
startActivityForResult(new Intent(getContext(), Login.class), Constants.LOGIN_RESULT_CODE);
});
final FragmentManager fragmentManager = getChildFragmentManager();
dialogFragment.show(fragmentManager, "accountSwitcher");
}
private Preference getPreference(final int title,
@ -311,6 +221,7 @@ public class MorePreferencesFragment extends BasePreferencesFragment {
return preference;
}
public static class MoreHeaderPreference extends Preference {
public MoreHeaderPreference(final Context context) {

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/accounts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/add_account_btn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="5"
tools:listitem="@layout/pref_account_switcher" />
<com.google.android.material.button.MaterialButton
android:id="@+id/add_account_btn"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add_account"
app:icon="@drawable/ic_add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accounts" />
</androidx.constraintlayout.widget.ConstraintLayout>