diff --git a/app/src/main/java/i2p/bote/android/EmailListAdapter.java b/app/src/main/java/i2p/bote/android/EmailListAdapter.java index 4dbd298..629896c 100644 --- a/app/src/main/java/i2p/bote/android/EmailListAdapter.java +++ b/app/src/main/java/i2p/bote/android/EmailListAdapter.java @@ -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 { - private final LayoutInflater mInflater; - private EmailSelector mSelector; +public class EmailListAdapter extends MultiSelectionUtil.SelectableAdapter { + 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 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 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. + *

+ * 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 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 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(); } } diff --git a/app/src/main/java/i2p/bote/android/EmailListFragment.java b/app/src/main/java/i2p/bote/android/EmailListFragment.java index e845158..54899c7 100644 --- a/app/src/main/java/i2p/bote/android/EmailListFragment.java +++ b/app/src/main/java/i2p/bote/android/EmailListFragment.java @@ -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>, 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 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 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 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 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> 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); diff --git a/app/src/main/java/i2p/bote/android/addressbook/AddressBookFragment.java b/app/src/main/java/i2p/bote/android/addressbook/AddressBookFragment.java index c70d769..36df99e 100644 --- a/app/src/main/java/i2p/bote/android/addressbook/AddressBookFragment.java +++ b/app/src/main/java/i2p/bote/android/addressbook/AddressBookFragment.java @@ -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> { 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> loader, SortedSet data) { - mAdapter.setData(data); - - if (isResumed()) { - setListShown(true); - } else { - setListShownNoAnimation(true); - } + mAdapter.setContacts(data); } @Override public void onLoaderReset(Loader> loader) { - mAdapter.setData(null); + mAdapter.setContacts(null); } } diff --git a/app/src/main/java/i2p/bote/android/addressbook/ContactAdapter.java b/app/src/main/java/i2p/bote/android/addressbook/ContactAdapter.java index a89e397..b80057a 100644 --- a/app/src/main/java/i2p/bote/android/addressbook/ContactAdapter.java +++ b/app/src/main/java/i2p/bote/android/addressbook/ContactAdapter.java @@ -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 { - private final LayoutInflater mInflater; +public class ContactAdapter extends RecyclerView.Adapter { + private List 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 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 contacts) { + if (contacts != null) { + mContacts = new ArrayList(); + 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; } } diff --git a/app/src/main/java/i2p/bote/android/util/AuthenticatedListFragment.java b/app/src/main/java/i2p/bote/android/util/AuthenticatedFragment.java similarity index 57% rename from app/src/main/java/i2p/bote/android/util/AuthenticatedListFragment.java rename to app/src/main/java/i2p/bote/android/util/AuthenticatedFragment.java index f270a9d..fe29653 100644 --- a/app/src/main/java/i2p/bote/android/util/AuthenticatedListFragment.java +++ b/app/src/main/java/i2p/bote/android/util/AuthenticatedFragment.java @@ -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(); } diff --git a/app/src/main/java/i2p/bote/android/util/MultiSelectionUtil.java b/app/src/main/java/i2p/bote/android/util/MultiSelectionUtil.java index 641a3b7..2ff2165 100644 --- a/app/src/main/java/i2p/bote/android/util/MultiSelectionUtil.java +++ b/app/src/main/java/i2p/bote/android/util/MultiSelectionUtil.java @@ -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 listView, activity + * Attach a Controller to the given recyclerView, activity * and listener. * - * @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> 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>(); + mItemsToCheck.add(new Pair(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 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 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>(); } - mItemsToCheck.add(new Pair(pos, adapter.getItemId(pos))); + mItemsToCheck.add(new Pair(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 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>(); - mItemsToCheck.add(new Pair(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 extends RecyclerView.Adapter { + 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 getSelectedItems() { + List items = + new ArrayList(selectedItems.size()); + for (int i = 0; i < selectedItems.size(); i++) { + items.add(selectedItems.keyAt(i)); + } + return items; + } + } } diff --git a/app/src/main/res/drawable/listitem_checked.xml b/app/src/main/res/drawable/listitem_checked.xml index ad4b7af..34629af 100644 --- a/app/src/main/res/drawable/listitem_checked.xml +++ b/app/src/main/res/drawable/listitem_checked.xml @@ -1,7 +1,8 @@ + - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_list_contacts.xml b/app/src/main/res/layout/fragment_list_contacts.xml index 7a117d5..058ab1d 100644 --- a/app/src/main/res/layout/fragment_list_contacts.xml +++ b/app/src/main/res/layout/fragment_list_contacts.xml @@ -5,10 +5,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_height="match_parent" + android:scrollbars="vertical"/> - - + android:layout_height="match_parent" + android:scrollbars="vertical"/> + app:fab_icon="@drawable/ic_create_white_24dp"/> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_list_emails_with_refresh.xml b/app/src/main/res/layout/fragment_list_emails_with_refresh.xml index 3f4d222..f5da1e6 100644 --- a/app/src/main/res/layout/fragment_list_emails_with_refresh.xml +++ b/app/src/main/res/layout/fragment_list_emails_with_refresh.xml @@ -1,7 +1,8 @@ - @@ -9,23 +10,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - - + android:layout_height="match_parent" + android:scrollbars="vertical"/> + app:fab_icon="@drawable/ic_create_white_24dp"/> diff --git a/app/src/main/res/menu/authenticated_list.xml b/app/src/main/res/menu/authenticated_fragment.xml similarity index 100% rename from app/src/main/res/menu/authenticated_list.xml rename to app/src/main/res/menu/authenticated_fragment.xml diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 12c35c5..a99ca40 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,6 +7,7 @@ #ff9100 #e0e0e0 + #bdbdbd #c31756 #f00