Migrate AuthenticatedListFragments to RecyclerView

This commit is contained in:
str4d
2015-01-07 04:22:43 +00:00
parent f74a5befbe
commit c493478c08
12 changed files with 570 additions and 491 deletions

View File

@@ -3,10 +3,11 @@ package i2p.bote.android;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.os.Build;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
@@ -19,169 +20,286 @@ import java.util.List;
import javax.mail.Part;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.MultiSelectionUtil;
import i2p.bote.email.Email;
public class EmailListAdapter extends ArrayAdapter<Email> {
private final LayoutInflater mInflater;
private EmailSelector mSelector;
public class EmailListAdapter extends MultiSelectionUtil.SelectableAdapter<RecyclerView.ViewHolder> {
private static final DateFormat DATE_BEFORE_THIS_YEAR = DateFormat.getDateInstance(DateFormat.MEDIUM);
private static final DateFormat DATE_THIS_YEAR = new SimpleDateFormat(
((SimpleDateFormat) SimpleDateFormat.getDateInstance(DateFormat.MEDIUM))
.toPattern().replaceAll(",?\\W?[Yy]+\\W?", "")
);
private static final DateFormat DATE_TODAY = DateFormat.getTimeInstance();
private Calendar BOUNDARY_DAY;
private Calendar BOUNDARY_YEAR;
private Context mCtx;
private String mFolderName;
private EmailListFragment.OnEmailSelectedListener mListener;
private boolean mIsOutbox;
private List<Email> mEmails;
private int mIncompleteEmails;
public interface EmailSelector {
public boolean inActionMode();
public void select(View view);
}
public EmailListAdapter(Context context, EmailSelector selector, boolean isOutbox) {
super(context, android.R.layout.simple_list_item_2);
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mSelector = selector;
mIsOutbox = isOutbox;
}
public void setData(List<Email> emails) {
clear();
if (emails != null) {
for (Email email : emails) {
add(email);
}
public static class IncompleteEmailViewHolder extends RecyclerView.ViewHolder {
public IncompleteEmailViewHolder(View itemView) {
super(itemView);
}
}
private static class ViewHolder {
ImageView picture;
//View emailSelected;
TextView subject;
TextView address;
TextView content;
TextView sent;
View emailAttachment;
TextView emailStatus;
View emailDelivered;
public static class EmailViewHolder extends RecyclerView.ViewHolder {
public ImageView picture;
//public ImageView emailSelected;
public TextView subject;
public TextView address;
public TextView content;
public TextView sent;
public ImageView emailAttachment;
public TextView emailStatus;
public ImageView emailDelivered;
public EmailViewHolder(View itemView) {
super(itemView);
picture = (ImageView) itemView.findViewById(R.id.contact_picture);
//emailSelected = view.findViewById(R.id.email_selected);
subject = (TextView) itemView.findViewById(R.id.email_subject);
address = (TextView) itemView.findViewById(R.id.email_address);
content = (TextView) itemView.findViewById(R.id.email_content);
sent = (TextView) itemView.findViewById(R.id.email_sent);
emailAttachment = (ImageView) itemView.findViewById(R.id.email_attachment);
emailStatus = (TextView) itemView.findViewById(R.id.email_status);
emailDelivered = (ImageView) itemView.findViewById(R.id.email_delivered);
}
}
public EmailListAdapter(Context context, String folderName,
EmailListFragment.OnEmailSelectedListener listener) {
super();
mCtx = context;
mFolderName = folderName;
mListener = listener;
mIsOutbox = BoteHelper.isOutbox(folderName);
mIncompleteEmails = 0;
setHasStableIds(true);
setDateBoundaries();
}
/**
* Set up the boundaries for date display formats.
* <p/>
* TODO: call this method at midnight to refresh the UI
*/
public void setDateBoundaries() {
BOUNDARY_DAY = Calendar.getInstance();
BOUNDARY_DAY.set(Calendar.HOUR, 0);
BOUNDARY_DAY.set(Calendar.MINUTE, 0);
BOUNDARY_DAY.set(Calendar.SECOND, 0);
BOUNDARY_YEAR = Calendar.getInstance();
BOUNDARY_YEAR.set(Calendar.MONTH, Calendar.JANUARY);
BOUNDARY_YEAR.set(Calendar.DAY_OF_MONTH, 1);
BOUNDARY_YEAR.set(Calendar.HOUR, 0);
BOUNDARY_YEAR.set(Calendar.MINUTE, 0);
BOUNDARY_YEAR.set(Calendar.SECOND, 0);
if (mEmails != null)
notifyDataSetChanged();
}
public void setEmails(List<Email> emails) {
mEmails = emails;
notifyDataSetChanged();
}
public Email getEmail(int position) {
if (mIncompleteEmails > 0)
position--;
if (position < 0)
return null;
return mEmails.get(position);
}
public void setIncompleteEmails(int incompleteEmails) {
if (incompleteEmails > 0) {
if (mIncompleteEmails == 0) {
mIncompleteEmails = incompleteEmails;
notifyItemInserted(0);
} else {
mIncompleteEmails = incompleteEmails;
notifyItemChanged(0);
}
} else if (mIncompleteEmails > 0) {
mIncompleteEmails = 0;
notifyItemRemoved(0);
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
View view;
public int getItemViewType(int position) {
if (mIncompleteEmails > 0)
position--;
if (convertView == null) {
holder = new ViewHolder();
view = mInflater.inflate(R.layout.listitem_email, parent, false);
holder.picture = (ImageView) view.findViewById(R.id.contact_picture);
//holder.emailSelected = view.findViewById(R.id.email_selected);
holder.subject = (TextView) view.findViewById(R.id.email_subject);
holder.address = (TextView) view.findViewById(R.id.email_address);
holder.content = (TextView) view.findViewById(R.id.email_content);
holder.sent = (TextView) view.findViewById(R.id.email_sent);
holder.emailAttachment = view.findViewById(R.id.email_attachment);
holder.emailStatus = (TextView) view.findViewById(R.id.email_status);
holder.emailDelivered = view.findViewById(R.id.email_delivered);
view.setTag(holder);
} else {
view = convertView;
holder = (ViewHolder) view.getTag();
return position < 0 ? R.layout.listitem_incomplete : R.layout.listitem_email;
}
// Create new views (invoked by the layout manager)
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(viewType, parent, false);
switch (viewType) {
case R.layout.listitem_incomplete:
return new IncompleteEmailViewHolder(v);
case R.layout.listitem_email:
default:
return new EmailViewHolder(v);
}
}
final Email email = getItem(position);
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case R.layout.listitem_incomplete:
((TextView) holder.itemView).setText(
mCtx.getResources().getQuantityString(R.plurals.incomplete_emails,
mIncompleteEmails, mIncompleteEmails));
break;
if (!mSelector.inActionMode())
holder.picture.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
mSelector.select(view);
}
});
case R.layout.listitem_email:
final EmailViewHolder evh = (EmailViewHolder) holder;
final Email email = getEmail(position);
// TODO fix
//if (mSelectedEmails.get(position)) {
// holder.emailSelected.setVisibility(View.VISIBLE);
//}
try {
String otherAddress;
if (BoteHelper.isSentEmail(email))
otherAddress = email.getOneRecipient();
else
otherAddress = email.getOneFromAddress();
Bitmap pic = BoteHelper.getPictureForAddress(otherAddress);
if (pic != null)
holder.picture.setImageBitmap(pic);
else if (BoteHelper.isSentEmail(email) || !email.isAnonymous()) {
ViewGroup.LayoutParams lp = holder.picture.getLayoutParams();
holder.picture.setImageBitmap(BoteHelper.getIdenticonForAddress(otherAddress, lp.width, lp.height));
} else
holder.picture.setImageDrawable(
getContext().getResources().getDrawable(R.drawable.ic_contact_picture));
holder.subject.setText(email.getSubject());
holder.address.setText(BoteHelper.getNameAndShortDestination(otherAddress));
Date date = email.getSentDate();
if (date == null)
date = email.getReceivedDate();
if (date != null) {
DateFormat df;
Calendar boundary = Calendar.getInstance();
boundary.set(Calendar.HOUR, 0);
boundary.set(Calendar.MINUTE, 0);
boundary.set(Calendar.SECOND, 0);
if (date.before(boundary.getTime())) {
boundary.set(Calendar.MONTH, Calendar.JANUARY);
boundary.set(Calendar.DAY_OF_MONTH, 1);
if (date.before(boundary.getTime())) // Sent before this year
df = DateFormat.getDateInstance(DateFormat.MEDIUM);
else { // Sent this year before today
String yearlessPattern = ((SimpleDateFormat) SimpleDateFormat.getDateInstance(DateFormat.MEDIUM))
.toPattern().replaceAll(",?\\W?[Yy]+\\W?", "");
df = new SimpleDateFormat(yearlessPattern);
evh.picture.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
selectEmail(evh.getPosition(), evh.getItemId(), true);
}
} else // Sent today
df = DateFormat.getTimeInstance();
holder.sent.setText(df.format(date));
holder.sent.setVisibility(View.VISIBLE);
} else
holder.sent.setVisibility(View.GONE);
});
evh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
selectEmail(evh.getPosition(), evh.getItemId(), false);
}
});
evh.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
selectEmail(evh.getPosition(), evh.getItemId(), true);
return true;
}
});
holder.emailAttachment.setVisibility(View.GONE);
List<Part> parts = email.getParts();
for (Part part : parts) {
if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
holder.emailAttachment.setVisibility(View.VISIBLE);
break;
}
}
holder.subject.setTypeface(email.isUnread() ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
holder.address.setTypeface(email.isUnread() ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
if (email.isAnonymous() && !BoteHelper.isSentEmail(email)) {
if (email.isUnread())
holder.address.setTypeface(Typeface.DEFAULT, Typeface.BOLD_ITALIC);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
evh.itemView.setSelected(isSelected(position));
else
holder.address.setTypeface(Typeface.DEFAULT, Typeface.ITALIC);
}
evh.itemView.setActivated(isSelected(position));
// TODO fix
//holder.emailSelected.setVisibility(isSelected(position) ? View.VISIBLE : View.GONE);
// Set email sending status if this is the outbox,
// or set email delivery status if we sent it.
if (mIsOutbox) {
holder.emailStatus.setText(BoteHelper.getEmailStatusText(
getContext(), email, false));
holder.emailStatus.setVisibility(View.VISIBLE);
} else if (BoteHelper.isSentEmail(email)) {
if (email.isDelivered()) {
holder.emailStatus.setVisibility(View.GONE);
} else {
holder.emailStatus.setText(email.getDeliveryPercentage() + "%");
holder.emailStatus.setVisibility(View.VISIBLE);
try {
boolean isSentEmail = BoteHelper.isSentEmail(email);
String otherAddress;
if (isSentEmail)
otherAddress = email.getOneRecipient();
else
otherAddress = email.getOneFromAddress();
Bitmap pic = BoteHelper.getPictureForAddress(otherAddress);
if (pic != null)
evh.picture.setImageBitmap(pic);
else if (isSentEmail || !email.isAnonymous()) {
ViewGroup.LayoutParams lp = evh.picture.getLayoutParams();
evh.picture.setImageBitmap(BoteHelper.getIdenticonForAddress(otherAddress, lp.width, lp.height));
} else
evh.picture.setImageDrawable(
mCtx.getResources().getDrawable(R.drawable.ic_contact_picture));
evh.subject.setText(email.getSubject());
evh.address.setText(BoteHelper.getNameAndShortDestination(otherAddress));
Date date = email.getSentDate();
if (date == null)
date = email.getReceivedDate();
if (date != null) {
DateFormat df;
if (date.before(BOUNDARY_DAY.getTime())) {
if (date.before(BOUNDARY_YEAR.getTime())) // Sent before this year
df = DATE_BEFORE_THIS_YEAR;
else // Sent this year before today
df = DATE_THIS_YEAR;
} else // Sent today
df = DATE_TODAY;
evh.sent.setText(df.format(date));
evh.sent.setVisibility(View.VISIBLE);
} else
evh.sent.setVisibility(View.GONE);
evh.emailAttachment.setVisibility(View.GONE);
for (Part part : email.getParts()) {
if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
evh.emailAttachment.setVisibility(View.VISIBLE);
break;
}
}
evh.subject.setTypeface(email.isUnread() ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
evh.address.setTypeface(email.isUnread() ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
if (email.isAnonymous() && !isSentEmail) {
if (email.isUnread())
evh.address.setTypeface(Typeface.DEFAULT, Typeface.BOLD_ITALIC);
else
evh.address.setTypeface(Typeface.DEFAULT, Typeface.ITALIC);
}
// Set email sending status if this is the outbox,
// or set email delivery status if we sent it.
if (mIsOutbox) {
evh.emailStatus.setText(BoteHelper.getEmailStatusText(
mCtx, email, false));
evh.emailStatus.setVisibility(View.VISIBLE);
} else if (isSentEmail) {
if (email.isDelivered()) {
evh.emailStatus.setVisibility(View.GONE);
} else {
evh.emailStatus.setText(email.getDeliveryPercentage() + "%");
evh.emailStatus.setVisibility(View.VISIBLE);
}
}
evh.emailDelivered.setVisibility(
!mIsOutbox && isSentEmail && email.isDelivered() ?
View.VISIBLE : View.GONE);
} catch (Exception e) {
evh.subject.setText("ERROR: " + e.getMessage());
}
}
holder.emailDelivered.setVisibility(
!mIsOutbox && BoteHelper.isSentEmail(email) && email.isDelivered() ?
View.VISIBLE : View.GONE);
} catch (Exception e) {
holder.subject.setText("ERROR: " + e.getMessage());
evh.content.setText(email.getText());
break;
}
holder.content.setText(email.getText());
}
return view;
private void selectEmail(int position, long id, boolean selectorOnly) {
if (selectorOnly || getSelector().inActionMode()) {
getSelector().selectItem(position, id);
} else {
final Email email = getEmail(position);
mListener.onEmailSelected(mFolderName, email.getMessageID());
}
}
// Return the size of the dataset (invoked by the layout manager)
@Override
public int getItemCount() {
if (mEmails != null)
return mIncompleteEmails > 0 ? mEmails.size() + 1 : mEmails.size();
return 0;
}
public long getItemId(int position) {
Email email = getEmail(position);
return email == null ? 0 : email.getMessageID().hashCode();
}
}

View File

@@ -4,7 +4,6 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.LoaderManager;
@@ -12,18 +11,16 @@ import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.view.ActionMode;
import android.util.SparseBooleanArray;
import android.view.Gravity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import net.i2p.I2PAppContext;
@@ -37,7 +34,7 @@ import javax.mail.Flags.Flag;
import javax.mail.MessagingException;
import i2p.bote.I2PBote;
import i2p.bote.android.util.AuthenticatedListFragment;
import i2p.bote.android.util.AuthenticatedFragment;
import i2p.bote.android.util.BetterAsyncTaskLoader;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.MoveToDialogFragment;
@@ -48,10 +45,10 @@ import i2p.bote.fileencryption.PasswordException;
import i2p.bote.folder.EmailFolder;
import i2p.bote.folder.FolderListener;
public class EmailListFragment extends AuthenticatedListFragment implements
public class EmailListFragment extends AuthenticatedFragment implements
LoaderManager.LoaderCallbacks<List<Email>>,
MoveToDialogFragment.MoveToDialogListener,
EmailListAdapter.EmailSelector, SwipeRefreshLayout.OnRefreshListener {
SwipeRefreshLayout.OnRefreshListener {
public static final String FOLDER_NAME = "folder_name";
private static final int EMAIL_LIST_LOADER = 1;
@@ -60,9 +57,8 @@ public class EmailListFragment extends AuthenticatedListFragment implements
private MultiSwipeRefreshLayout mSwipeRefreshLayout;
private AsyncTask<Void, Void, Void> mCheckingTask;
private TextView mEmptyText;
private TextView mNumIncompleteEmails;
private RecyclerView mEmailsList;
private EmailListAdapter mAdapter;
private EmailFolder mFolder;
@@ -101,10 +97,8 @@ public class EmailListFragment extends AuthenticatedListFragment implements
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Create the list fragment's content view by calling the super method
final View listFragmentView = super.onCreateView(inflater, container, savedInstanceState);
public View onCreateAuthenticatedView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
String folderName = getArguments().getString(FOLDER_NAME);
mFolder = BoteHelper.getMailFolder(folderName);
boolean isInbox = BoteHelper.isInbox(mFolder);
@@ -112,8 +106,8 @@ public class EmailListFragment extends AuthenticatedListFragment implements
View v = inflater.inflate(
isInbox ? R.layout.fragment_list_emails_with_refresh : R.layout.fragment_list_emails,
container, false);
FrameLayout listContainer = (FrameLayout) v.findViewById(R.id.list_container);
listContainer.addView(listFragmentView);
mEmailsList = (RecyclerView) v.findViewById(R.id.emails_list);
mNewEmail = (ImageButton) v.findViewById(R.id.promoted_action);
mNewEmail.setOnClickListener(new View.OnClickListener() {
@@ -126,56 +120,47 @@ public class EmailListFragment extends AuthenticatedListFragment implements
if (isInbox) {
mSwipeRefreshLayout = (MultiSwipeRefreshLayout) v;
// Set up the empty view
View emptyView = mSwipeRefreshLayout.findViewById(android.R.id.empty);
ListView listView = (ListView) mSwipeRefreshLayout.findViewById(android.R.id.list);
listView.setEmptyView(emptyView);
mEmptyText = (TextView) mSwipeRefreshLayout.findViewById(R.id.empty_text);
// Set up the MultiSwipeRefreshLayout
mSwipeRefreshLayout.setColorSchemeResources(
R.color.primary, R.color.accent, R.color.primary, R.color.accent);
mSwipeRefreshLayout.setSwipeableChildren(android.R.id.list, android.R.id.empty);
mSwipeRefreshLayout.setSwipeableChildren(R.id.emails_list);
mSwipeRefreshLayout.setOnRefreshListener(this);
}
return v;
}
@Override
public void setEmptyText(CharSequence text) {
if (mEmptyText == null)
super.setEmptyText(text);
else
mEmptyText.setText(text);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mAdapter = new EmailListAdapter(getActivity(), this,
BoteHelper.isOutbox(mFolder));
setListAdapter(mAdapter);
// Use a linear layout manager
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getActivity());
mEmailsList.setLayoutManager(mLayoutManager);
mEmailsList.setHasFixedSize(true);
// Set the adapter for the list view
mAdapter = new EmailListAdapter(getActivity(), mFolder.getName(), mCallback);
mEmailsList.setAdapter(mAdapter);
// Attach a MultiSelectionUtil.Controller to the ListView, giving it an instance of
// ModalChoiceListener (see below)
mModalChoiceListener = new ModalChoiceListener();
mMultiSelectController = MultiSelectionUtil
.attachMultiSelectionController(getListView(), (ActionBarActivity) getActivity(),
.attachMultiSelectionController(mEmailsList, (ActionBarActivity) getActivity(),
mModalChoiceListener);
// Allow the Controller to restore itself
mMultiSelectController.restoreInstanceState(savedInstanceState);
if (mFolder == null) {
setEmptyText(getResources().getString(
R.string.folder_does_not_exist));
getActivity().setTitle(getResources().getString(R.string.app_name));
} else {
getActivity().setTitle(
BoteHelper.getFolderDisplayName(getActivity(), mFolder));
mFolder = I2PBote.getInstance().getInbox();
Toast.makeText(getActivity(), R.string.folder_does_not_exist, Toast.LENGTH_SHORT).show();
}
getActivity().setTitle(
BoteHelper.getFolderDisplayName(getActivity(), mFolder));
}
@Override
@@ -206,32 +191,19 @@ public class EmailListFragment extends AuthenticatedListFragment implements
* Only called when we have a password cached, or no
* password is required.
*/
protected void onInitializeList() {
protected void onInitializeFragment() {
if (mFolder == null)
return;
if (BoteHelper.isInbox(mFolder)) {
int numIncompleteEmails = I2PBote.getInstance().getNumIncompleteEmails();
if (numIncompleteEmails > 0) {
mNumIncompleteEmails = (TextView) getActivity().getLayoutInflater().inflate(
R.layout.listitem_incomplete, getListView(), false);
mNumIncompleteEmails.setText(getResources().getQuantityString(R.plurals.incomplete_emails,
numIncompleteEmails, numIncompleteEmails));
getListView().addHeaderView(mNumIncompleteEmails, null, false);
}
mAdapter.setIncompleteEmails(I2PBote.getInstance().getNumIncompleteEmails());
}
setListShown(false);
setEmptyText(getResources().getString(
R.string.folder_empty));
getLoaderManager().initLoader(EMAIL_LIST_LOADER, null, this);
}
protected void onDestroyList() {
if (mNumIncompleteEmails != null) {
getListView().removeHeaderView(mNumIncompleteEmails);
mNumIncompleteEmails = null;
}
protected void onDestroyFragment() {
mAdapter.setIncompleteEmails(0);
getLoaderManager().destroyLoader(EMAIL_LIST_LOADER);
}
@@ -292,33 +264,17 @@ public class EmailListFragment extends AuthenticatedListFragment implements
startActivity(nei);
}
@Override
public void onListItemClick(ListView parent, View view, int pos, long id) {
super.onListItemClick(parent, view, pos, id);
final Email email = (Email) getListView().getItemAtPosition(pos);
if (email != null)
mCallback.onEmailSelected(mFolder.getName(), email.getMessageID());
}
private class ModalChoiceListener implements MultiSelectionUtil.MultiChoiceModeListener {
private boolean areUnread;
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
final ListView listView = getListView();
int numChecked = 0;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
final SparseBooleanArray items = listView.getCheckedItemPositions();
for (int i = 0; i < items.size(); i++)
if (items.valueAt(i))
numChecked++;
} else
numChecked = listView.getCheckedItemCount();
int numChecked = mAdapter.getSelectedItemCount();
mode.setTitle(getResources().getString(R.string.items_selected, numChecked));
if (checked && numChecked == 1) { // This is the first checked item
Email email = (Email) listView.getItemAtPosition(position);
Email email = mAdapter.getEmail(position);
areUnread = email.isUnread();
mode.invalidate();
}
@@ -326,44 +282,39 @@ public class EmailListFragment extends AuthenticatedListFragment implements
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
final ListView listView = getListView();
// Respond to clicks on the actions in the CAB
switch (item.getItemId()) {
case R.id.action_delete:
SparseBooleanArray toDelete = listView.getCheckedItemPositions();
List<Integer> toDelete = mAdapter.getSelectedItems();
if (toDelete.size() == 0)
return false;
for (int i = (toDelete.size() - 1); i >= 0; i--) {
if (toDelete.valueAt(i)) {
Email email = (Email) listView.getItemAtPosition(toDelete.keyAt(i));
BoteHelper.revokeAttachmentUriPermissions(
getActivity(),
mFolder.getName(),
email);
// The Loader will update mAdapter
I2PBote.getInstance().deleteEmail(mFolder, email.getMessageID());
}
Email email = mAdapter.getEmail(toDelete.get(i));
BoteHelper.revokeAttachmentUriPermissions(
getActivity(),
mFolder.getName(),
email);
// The Loader will update mAdapter
I2PBote.getInstance().deleteEmail(mFolder, email.getMessageID());
}
mode.finish();
return true;
case R.id.action_mark_read:
case R.id.action_mark_unread:
SparseBooleanArray selected = listView.getCheckedItemPositions();
List<Integer> selected = mAdapter.getSelectedItems();
for (int i = (selected.size() - 1); i >= 0; i--) {
if (selected.valueAt(i)) {
Email email = (Email) listView.getItemAtPosition(selected.keyAt(i));
try {
// The Loader will update mAdapter
mFolder.setNew(email, !areUnread);
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Email email = mAdapter.getEmail(selected.get(i));
try {
// The Loader will update mAdapter
mFolder.setNew(email, !areUnread);
} catch (PasswordException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GeneralSecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
areUnread = !areUnread;
@@ -415,13 +366,10 @@ public class EmailListFragment extends AuthenticatedListFragment implements
// Called by EmailListActivity.onFolderSelected()
public void onFolderSelected(EmailFolder newFolder) {
final ListView listView = getListView();
SparseBooleanArray toMove = listView.getCheckedItemPositions();
List<Integer> toMove = mAdapter.getSelectedItems();
for (int i = (toMove.size() - 1); i >= 0; i--) {
if (toMove.valueAt(i)) {
Email email = (Email) listView.getItemAtPosition(toMove.keyAt(i));
mFolder.move(email, newFolder);
}
Email email = mAdapter.getEmail(toMove.get(i));
mFolder.move(email, newFolder);
}
mMultiSelectController.finish();
}
@@ -491,7 +439,7 @@ public class EmailListFragment extends AuthenticatedListFragment implements
// TODO Auto-generated catch block
e.printStackTrace();
}
mAdapter.setData(data);
mAdapter.setEmails(data);
try {
getActivity().setTitle(
BoteHelper.getFolderDisplayNameWithNew(getActivity(), mFolder));
@@ -501,33 +449,14 @@ public class EmailListFragment extends AuthenticatedListFragment implements
if (log.shouldLog(Log.WARN))
log.warn("Email list loader finished, but password is no longer cached", e);
}
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
public void onLoaderReset(Loader<List<Email>> loader) {
mAdapter.setData(null);
mAdapter.setEmails(null);
getActivity().setTitle(
BoteHelper.getFolderDisplayName(getActivity(), mFolder));
}
// EmailListAdapter.EmailSelector
public boolean inActionMode() {
return mMultiSelectController.inActionMode();
}
public void select(View view) {
final ListView listView = getListView();
final int position = listView.getPositionForView(view);
listView.setItemChecked(position, !listView.isItemChecked(position));
view.performLongClick();
}
// SwipeRefreshLayout.OnRefreshListener
public void onRefresh() {
@@ -560,19 +489,7 @@ public class EmailListFragment extends AuthenticatedListFragment implements
protected void onPostExecute(Void result) {
super.onPostExecute(result);
int numIncomingEmails = I2PBote.getInstance().getNumIncompleteEmails();
if (numIncomingEmails > 0) {
if (mNumIncompleteEmails == null) {
mNumIncompleteEmails = new TextView(getActivity());
getListView().addHeaderView(mNumIncompleteEmails);
}
mNumIncompleteEmails.setText(getResources().getQuantityString(
R.plurals.incomplete_emails,
numIncomingEmails, numIncomingEmails));
} else if (mNumIncompleteEmails != null) {
getListView().removeHeaderView(mNumIncompleteEmails);
mNumIncompleteEmails = null;
}
mAdapter.setIncompleteEmails(I2PBote.getInstance().getNumIncompleteEmails());
// Notify PullToRefreshLayout that the refresh has finished
mSwipeRefreshLayout.setRefreshing(false);

View File

@@ -6,14 +6,14 @@ import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ListView;
import com.google.zxing.integration.android.IntentIntegrator;
@@ -21,14 +21,15 @@ import java.util.SortedSet;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
import i2p.bote.android.util.AuthenticatedListFragment;
import i2p.bote.android.util.AuthenticatedFragment;
import i2p.bote.android.util.BetterAsyncTaskLoader;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.packet.dht.Contact;
public class AddressBookFragment extends AuthenticatedListFragment implements
public class AddressBookFragment extends AuthenticatedFragment implements
LoaderManager.LoaderCallbacks<SortedSet<Contact>> {
OnContactSelectedListener mCallback;
private RecyclerView mContactsList;
private ContactAdapter mAdapter;
private View mPromotedActions;
@@ -60,14 +61,11 @@ public class AddressBookFragment extends AuthenticatedListFragment implements
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Create the list fragment's content view by calling the super method
final View listFragmentView = super.onCreateView(inflater, container, savedInstanceState);
public View onCreateAuthenticatedView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_list_contacts, container, false);
FrameLayout listContainer = (FrameLayout) v.findViewById(R.id.list_container);
listContainer.addView(listFragmentView);
mContactsList = (RecyclerView) v.findViewById(R.id.contacts_list);
mPromotedActions = v.findViewById(R.id.promoted_actions);
ImageButton b = (ImageButton) v.findViewById(R.id.action_new_contact);
@@ -92,9 +90,14 @@ public class AddressBookFragment extends AuthenticatedListFragment implements
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mAdapter = new ContactAdapter(getActivity());
setListAdapter(mAdapter);
// Use a linear layout manager
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getActivity());
mContactsList.setLayoutManager(mLayoutManager);
// Set the adapter for the list view
mAdapter = new ContactAdapter(mCallback);
mContactsList.setAdapter(mAdapter);
}
/**
@@ -102,14 +105,11 @@ public class AddressBookFragment extends AuthenticatedListFragment implements
* Only called when we have a password cached, or no
* password is required.
*/
protected void onInitializeList() {
setListShown(false);
setEmptyText(getResources().getString(
R.string.address_book_empty));
protected void onInitializeFragment() {
getLoaderManager().initLoader(0, null, this);
}
protected void onDestroyList() {
protected void onDestroyFragment() {
getLoaderManager().destroyLoader(0);
}
@@ -135,13 +135,7 @@ public class AddressBookFragment extends AuthenticatedListFragment implements
integrator.initiateScan(IntentIntegrator.QR_CODE_TYPES);
}
@Override
public void onListItemClick(ListView parent, View view, int pos, long id) {
mCallback.onContactSelected(mAdapter.getItem(pos));
}
protected void updateContactList() {
setListShown(false);
getLoaderManager().restartLoader(0, null, this);
}
@@ -184,17 +178,11 @@ public class AddressBookFragment extends AuthenticatedListFragment implements
@Override
public void onLoadFinished(Loader<SortedSet<Contact>> loader,
SortedSet<Contact> data) {
mAdapter.setData(data);
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
mAdapter.setContacts(data);
}
@Override
public void onLoaderReset(Loader<SortedSet<Contact>> loader) {
mAdapter.setData(null);
mAdapter.setContacts(null);
}
}

View File

@@ -1,54 +1,86 @@
package i2p.bote.android.addressbook;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
import i2p.bote.android.R;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.packet.dht.Contact;
public class ContactAdapter extends ArrayAdapter<Contact> {
private final LayoutInflater mInflater;
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ViewHolder> {
private List<Contact> mContacts;
private AddressBookFragment.OnContactSelectedListener mListener;
public ContactAdapter(Context context) {
super(context, android.R.layout.simple_list_item_2);
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public ImageView mPicture;
public TextView mName;
public void setData(SortedSet<Contact> contacts) {
clear();
if (contacts != null) {
for (Contact contact : contacts) {
add(contact);
}
public ViewHolder(View itemView) {
super(itemView);
mPicture = (ImageView) itemView.findViewById(R.id.contact_picture);
mName = (TextView) itemView.findViewById(R.id.contact_name);
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = mInflater.inflate(R.layout.listitem_contact, parent, false);
Contact contact = getItem(position);
public ContactAdapter(AddressBookFragment.OnContactSelectedListener listener) {
mListener = listener;
}
ImageView picture = (ImageView) v.findViewById(R.id.contact_picture);
TextView name = (TextView) v.findViewById(R.id.contact_name);
public void setContacts(SortedSet<Contact> contacts) {
if (contacts != null) {
mContacts = new ArrayList<Contact>();
mContacts.addAll(contacts);
} else
mContacts = null;
notifyDataSetChanged();
}
// Create new views (invoked by the layout manager)
@Override
public ContactAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.listitem_contact, parent, false);
return new ViewHolder(v);
}
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
Contact contact = mContacts.get(position);
String pic = contact.getPictureBase64();
if (pic != null && !pic.isEmpty())
picture.setImageBitmap(BoteHelper.decodePicture(pic));
holder.mPicture.setImageBitmap(BoteHelper.decodePicture(pic));
else {
ViewGroup.LayoutParams lp = picture.getLayoutParams();
picture.setImageBitmap(BoteHelper.getIdenticonForAddress(contact.getBase64Dest(), lp.width, lp.height));
ViewGroup.LayoutParams lp = holder.mPicture.getLayoutParams();
holder.mPicture.setImageBitmap(BoteHelper.getIdenticonForAddress(contact.getBase64Dest(), lp.width, lp.height));
}
name.setText(contact.getName());
holder.mName.setText(contact.getName());
return v;
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mListener.onContactSelected(mContacts.get(holder.getPosition()));
}
});
}
// Return the size of the dataset (invoked by the layout manager)
@Override
public int getItemCount() {
if (mContacts != null)
return mContacts.size();
return 0;
}
}

View File

@@ -1,18 +1,23 @@
package i2p.bote.android.util;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import i2p.bote.I2PBote;
import i2p.bote.android.R;
public abstract class AuthenticatedListFragment extends ListFragment {
public abstract class AuthenticatedFragment extends Fragment {
private FrameLayout mAuthenticatedView;
private MenuItem mLogIn;
private MenuItem mClearPassword;
private boolean mListInitialized;
private boolean mFragmentInitialized;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -20,16 +25,29 @@ public abstract class AuthenticatedListFragment extends ListFragment {
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_authenticated, container, false);
mAuthenticatedView = (FrameLayout) view.findViewById(R.id.authenticated_view);
mAuthenticatedView.addView(onCreateAuthenticatedView(inflater, container, savedInstanceState));
return view;
}
protected abstract View onCreateAuthenticatedView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);
@Override
public void onResume() {
super.onResume();
if (I2PBote.getInstance().isPasswordRequired()) {
// Ensure any existing data is destroyed.
destroyList();
destroyFragment();
} else {
// Password is cached, or not set.
initializeList();
initializeFragment();
}
getActivity().supportInvalidateOptionsMenu();
@@ -37,7 +55,7 @@ public abstract class AuthenticatedListFragment extends ListFragment {
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.authenticated_list, menu);
inflater.inflate(R.menu.authenticated_fragment, menu);
mLogIn = menu.findItem(R.id.action_log_in);
mClearPassword = menu.findItem(R.id.action_log_out);
}
@@ -56,7 +74,7 @@ public abstract class AuthenticatedListFragment extends ListFragment {
BoteHelper.requestPassword(getActivity(), new BoteHelper.RequestPasswordListener() {
@Override
public void onPasswordVerified() {
initializeList();
initializeFragment();
getActivity().supportInvalidateOptionsMenu();
}
@@ -68,7 +86,7 @@ public abstract class AuthenticatedListFragment extends ListFragment {
case R.id.action_log_out:
BoteHelper.clearPassword();
destroyList();
destroyFragment();
getActivity().supportInvalidateOptionsMenu();
return true;
@@ -77,24 +95,25 @@ public abstract class AuthenticatedListFragment extends ListFragment {
}
}
private void initializeList() {
if (mListInitialized)
private void initializeFragment() {
if (mFragmentInitialized)
return;
onInitializeList();
onInitializeFragment();
mListInitialized = true;
mAuthenticatedView.setVisibility(View.VISIBLE);
mFragmentInitialized = true;
}
private void destroyList() {
onDestroyList();
private void destroyFragment() {
onDestroyFragment();
setEmptyText(getResources().getString(
R.string.touch_lock_to_log_in));
mAuthenticatedView.setVisibility(View.GONE);
mListInitialized = false;
mFragmentInitialized = false;
}
protected abstract void onInitializeList();
protected abstract void onDestroyList();
protected abstract void onInitializeFragment();
protected abstract void onDestroyFragment();
}

View File

@@ -1,4 +1,5 @@
/*
/**
* Copyright (C) 2015 str4d
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,17 +20,16 @@ package i2p.bote.android.util;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.RecyclerView;
import android.util.Pair;
import android.util.SparseBooleanArray;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* Utilities for handling multiple selection in list views. Contains functionality similar to {@link
@@ -39,30 +39,35 @@ import java.util.HashSet;
public class MultiSelectionUtil {
/**
* Attach a Controller to the given <code>listView</code>, <code>activity</code>
* Attach a Controller to the given <code>recyclerView</code>, <code>activity</code>
* and <code>listener</code>.
*
* @param listView ListView which displays {@link android.widget.Checkable} items.
* @param recyclerView RecyclerView which displays {@link android.widget.Checkable} items.
* @param activity Activity which contains the ListView.
* @param listener Listener that will manage the selection mode.
* @return the attached Controller instance.
*/
public static Controller attachMultiSelectionController(final ListView listView,
public static Controller attachMultiSelectionController(final RecyclerView recyclerView,
final ActionBarActivity activity, final MultiChoiceModeListener listener) {
return new Controller(listView, activity, listener);
if (!(recyclerView.getAdapter() instanceof SelectableAdapter))
throw new IllegalArgumentException("Adapter must extend SelectableAdapter");
return new Controller(recyclerView, activity, listener);
}
public interface Selector {
public boolean inActionMode();
public void selectItem(int position, long id);
}
/**
* Class which provides functionality similar to {@link AbsListView#CHOICE_MODE_MULTIPLE_MODAL}
* for the {@link ListView} provided to it. A
* {@link android.widget.AdapterView.OnItemLongClickListener} is set on the ListView so that
* when an item is long-clicked an ActionBarCompat Action Mode is started. Once started, a
* {@link android.widget.AdapterView.OnItemClickListener} is set so that an item click toggles
* that item's checked state.
* for the {@link RecyclerView} provided to it.
*/
public static class Controller {
public static class Controller implements Selector {
private final ListView mListView;
private final RecyclerView mRecyclerView;
private final SelectableAdapter mAdapter;
private final ActionBarActivity mActivity;
private final MultiChoiceModeListener mListener;
private final Callbacks mCallbacks;
@@ -73,32 +78,47 @@ public class MultiSelectionUtil {
// Keeps record of any items that should be checked on the next action mode creation
private HashSet<Pair<Integer, Long>> mItemsToCheck;
// Reference to the replace OnItemClickListener (so it can be restored later)
private AdapterView.OnItemClickListener mOldItemClickListener;
private final Runnable mSetChoiceModeNoneRunnable = new Runnable() {
@Override
public void run() {
mListView.setChoiceMode(AbsListView.CHOICE_MODE_NONE);
}
};
private Controller(ListView listView, ActionBarActivity activity,
MultiChoiceModeListener listener) {
mListView = listView;
private Controller(RecyclerView recyclerView, ActionBarActivity activity,
MultiChoiceModeListener listener) {
mRecyclerView = recyclerView;
mAdapter = (SelectableAdapter) recyclerView.getAdapter();
mActivity = activity;
mListener = listener;
mCallbacks = new Callbacks();
// We set ourselves as the OnItemLongClickListener so we know when to start
// an Action Mode
listView.setOnItemLongClickListener(mCallbacks);
mAdapter.setSelector(this);
}
@Override
public boolean inActionMode() {
return mActionMode != null;
}
@Override
public void selectItem(int position, long id) {
if (mActionMode == null) {
mItemsToCheck = new HashSet<Pair<Integer, Long>>();
mItemsToCheck.add(new Pair<Integer, Long>(position, id));
mActionMode = mActivity.startSupportActionMode(mCallbacks);
} else {
mAdapter.toggleSelection(position);
// Check to see what the new checked state is, and then notify the listener
final boolean checked = mAdapter.isSelected(position);
mListener.onItemCheckedStateChanged(mActionMode, position, id, checked);
boolean hasCheckedItem = checked;
// Check to see if we have any checked items
if (!hasCheckedItem)
hasCheckedItem = mAdapter.getSelectedItemCount() > 0;
// If we don't have any checked items, finish the action mode
if (!hasCheckedItem)
mActionMode.finish();
}
}
/**
* Finish the current Action Mode (if there is one).
*/
@@ -138,31 +158,35 @@ public class MultiSelectionUtil {
* @param outState - The state passed to your Activity or Fragment.
*/
public void saveInstanceState(Bundle outState) {
if (mActionMode != null && mListView.getAdapter().hasStableIds()) {
outState.putLongArray(getStateKey(), mListView.getCheckedItemIds());
if (mActionMode != null && mAdapter.hasStableIds()) {
List<Integer> selectedItems = mAdapter.getSelectedItems();
long[] selectedItemIds = new long[selectedItems.size()];
for (int i = 0; i < selectedItems.size(); i++) {
selectedItemIds[i] = mAdapter.getItemId(selectedItems.get(i));
}
outState.putLongArray(getStateKey(), selectedItemIds);
}
}
// Internal utility methods
private String getStateKey() {
return MultiSelectionUtil.class.getSimpleName() + "_" + mListView.getId();
return MultiSelectionUtil.class.getSimpleName() + "_" + mRecyclerView.getId();
}
private void tryRestoreInstanceState(HashSet<Long> idsToCheckOnRestore) {
if (idsToCheckOnRestore == null || mListView.getAdapter() == null) {
if (idsToCheckOnRestore == null) {
return;
}
boolean idsFound = false;
Adapter adapter = mListView.getAdapter();
for (int pos = adapter.getCount() - 1; pos >= 0; pos--) {
if (idsToCheckOnRestore.contains(adapter.getItemId(pos))) {
for (int pos = mAdapter.getItemCount() - 1; pos >= 0; pos--) {
if (idsToCheckOnRestore.contains(mAdapter.getItemId(pos))) {
idsFound = true;
if (mItemsToCheck == null) {
mItemsToCheck = new HashSet<Pair<Integer, Long>>();
}
mItemsToCheck.add(new Pair<Integer, Long>(pos, adapter.getItemId(pos)));
mItemsToCheck.add(new Pair<Integer, Long>(pos, mAdapter.getItemId(pos)));
}
}
@@ -176,25 +200,16 @@ public class MultiSelectionUtil {
/**
* This class encapsulates all of the callbacks necessary for the controller class.
*/
final class Callbacks implements ActionMode.Callback, AdapterView.OnItemClickListener,
AdapterView.OnItemLongClickListener {
final class Callbacks implements ActionMode.Callback {
@Override
public final boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
if (mListener.onCreateActionMode(actionMode, menu)) {
mActionMode = actionMode;
// Keep a reference to the existing OnItemClickListener so we can restore it
mOldItemClickListener = mListView.getOnItemClickListener();
// Set-up the ListView to emulate CHOICE_MODE_MULTIPLE_MODAL
mListView.setOnItemClickListener(this);
mListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
mListView.removeCallbacks(mSetChoiceModeNoneRunnable);
// If there are some items to check, do it now
if (mItemsToCheck != null) {
for (Pair<Integer, Long> posAndId : mItemsToCheck) {
mListView.setItemChecked(posAndId.first, true);
mAdapter.toggleSelection(posAndId.first);
// Notify the listener that the item has been checked
mListener.onItemCheckedStateChanged(mActionMode, posAndId.first,
posAndId.second, true);
@@ -222,62 +237,10 @@ public class MultiSelectionUtil {
mListener.onDestroyActionMode(actionMode);
// Clear all the checked items
SparseBooleanArray checkedPositions = mListView.getCheckedItemPositions();
if (checkedPositions != null) {
for (int i = 0; i < checkedPositions.size(); i++) {
mListView.setItemChecked(checkedPositions.keyAt(i), false);
}
}
// Restore the original onItemClickListener
mListView.setOnItemClickListener(mOldItemClickListener);
mAdapter.clearSelections();
// Clear the Action Mode
mActionMode = null;
// Reset the ListView's Choice Mode
mListView.post(mSetChoiceModeNoneRunnable);
}
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
// Check to see what the new checked state is, and then notify the listener
final boolean checked = mListView.isItemChecked(position);
mListener.onItemCheckedStateChanged(mActionMode, position, id, checked);
boolean hasCheckedItem = checked;
// Check to see if we have any checked items
if (!hasCheckedItem) {
SparseBooleanArray checkedItemPositions = mListView.getCheckedItemPositions();
if (checkedItemPositions != null) {
// Iterate through the SparseBooleanArray to see if there is a checked item
int i = 0;
while (!hasCheckedItem && i < checkedItemPositions.size()) {
hasCheckedItem = checkedItemPositions.valueAt(i++);
}
}
}
// If we don't have any checked items, finish the action mode
if (!hasCheckedItem) {
mActionMode.finish();
}
}
@Override
public boolean onItemLongClick(AdapterView<?> adapterView, View view, int position,
long id) {
// If we already have an action mode started return false
// (onItemClick will be called anyway)
if (mActionMode != null) {
return false;
}
mItemsToCheck = new HashSet<Pair<Integer, Long>>();
mItemsToCheck.add(new Pair<Integer, Long>(position, id));
mActionMode = mActivity.startSupportActionMode(this);
return true;
}
}
}
@@ -294,4 +257,52 @@ public class MultiSelectionUtil {
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
boolean checked);
}
public static abstract class SelectableAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
private Selector mSelector;
private SparseBooleanArray selectedItems;
public SelectableAdapter() {
selectedItems = new SparseBooleanArray();
}
public void setSelector(Selector selector) {
mSelector = selector;
}
public Selector getSelector() {
return mSelector;
}
public void toggleSelection(int position) {
if (selectedItems.get(position, false)) {
selectedItems.delete(position);
} else {
selectedItems.put(position, true);
}
notifyItemChanged(position);
}
public boolean isSelected(int position) {
return selectedItems.get(position, false);
}
public void clearSelections() {
selectedItems.clear();
notifyDataSetChanged();
}
public int getSelectedItemCount() {
return selectedItems.size();
}
public List<Integer> getSelectedItems() {
List<Integer> items =
new ArrayList<Integer>(selectedItems.size());
for (int i = 0; i < selectedItems.size(); i++) {
items.add(selectedItems.keyAt(i));
}
return items;
}
}
}

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/listitem_pressed" android:state_pressed="true" />
<item android:drawable="@color/listitem_selected" android:state_activated="true" />
<item android:drawable="@color/listitem_selected" android:state_checked="true" />
<item android:drawable="@color/listitem_selected" android:state_selected="true" />
<item android:drawable="@android:color/transparent" />
</selector>

View File

@@ -5,10 +5,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/list_container"
<android.support.v7.widget.RecyclerView
android:id="@+id/contacts_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="match_parent"
android:scrollbars="vertical"/>
<net.i2p.android.ext.floatingactionbutton.FloatingActionsMenu
android:id="@+id/promoted_actions"

View File

@@ -1,13 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/list_container"
<android.support.v7.widget.RecyclerView
android:id="@+id/emails_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:scrollbars="vertical"/>
<net.i2p.android.ext.floatingactionbutton.FloatingActionButton
android:id="@+id/promoted_action"
@@ -21,5 +23,5 @@
android:layout_marginRight="@dimen/listitem_horizontal_margin"
app:fab_colorNormal="@color/accent"
app:fab_colorPressed="@color/accent_dark"
app:fab_icon="@drawable/ic_create_white_24dp" />
app:fab_icon="@drawable/ic_create_white_24dp"/>
</RelativeLayout>

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<i2p.bote.android.util.MultiSwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<i2p.bote.android.util.MultiSwipeRefreshLayout
android:id="@+id/swipe_refresh"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -9,23 +10,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/list_container"
<android.support.v7.widget.RecyclerView
android:id="@+id/emails_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ScrollView
android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/empty_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/folder_empty" />
</ScrollView>
android:layout_height="match_parent"
android:scrollbars="vertical"/>
<net.i2p.android.ext.floatingactionbutton.FloatingActionButton
android:id="@+id/promoted_action"
@@ -39,7 +28,7 @@
android:layout_marginRight="@dimen/listitem_horizontal_margin"
app:fab_colorNormal="@color/accent"
app:fab_colorPressed="@color/accent_dark"
app:fab_icon="@drawable/ic_create_white_24dp" />
app:fab_icon="@drawable/ic_create_white_24dp"/>
</RelativeLayout>
</i2p.bote.android.util.MultiSwipeRefreshLayout>

View File

@@ -7,6 +7,7 @@
<color name="accent_dark">#ff9100</color><!-- Orange A400 -->
<color name="listitem_selected">#e0e0e0</color><!-- Grey 300 -->
<color name="listitem_pressed">#bdbdbd</color><!-- Grey 400 -->
<color name="uva_color">#c31756</color>
<color name="error_color">#f00</color>