First try implementing sticky section headers

This commit is contained in:
Tobias Erthal
2016-09-02 22:41:45 +02:00
parent bf382ec59d
commit d5321a6fb2
8 changed files with 1045 additions and 19 deletions

View File

@@ -33,6 +33,7 @@ dependencies {
compile 'org.apache.james:apache-mime4j-dom:0.7.2'
compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0'
compile 'com.cocosw:bottomsheet:1.3.0@aar'
compile 'com.tonicartos:superslim:0.4.13'
// Material Drawer
compile 'com.mikepenz:materialdrawer:5.2.2@aar'

View File

@@ -35,6 +35,7 @@ import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.text.TextUtils;
import android.view.ActionMode;
@@ -54,6 +55,8 @@ import android.widget.ViewAnimator;
import com.getbase.floatingactionbutton.FloatingActionButton;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.tonicartos.superslim.LayoutManager;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
@@ -74,6 +77,7 @@ import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.adapter.KeySectionedListAdapter;
import org.sufficientlysecure.keychain.util.FabContainer;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Preferences;
@@ -97,8 +101,9 @@ public class KeyListFragment extends LoaderFragment
private static final int REQUEST_DELETE = 2;
private static final int REQUEST_VIEW_KEY = 3;
private KeyListAdapter mAdapter;
private StickyListHeadersListView mStickyList;
//private KeyListAdapter mAdapter;
private KeySectionedListAdapter mAdapter;
private RecyclerView mStickyList;
// saves the mode object for multiselect, needed for reset at some point
private ActionMode mActionMode = null;
@@ -125,8 +130,8 @@ public class KeyListFragment extends LoaderFragment
View root = super.onCreateView(inflater, superContainer, savedInstanceState);
View view = inflater.inflate(R.layout.key_list_fragment, getContainer());
mStickyList = (StickyListHeadersListView) view.findViewById(R.id.key_list_list);
mStickyList.setOnItemClickListener(this);
mStickyList = (RecyclerView) view.findViewById(R.id.key_list_list);
//mStickyList.setOnItemClickListener(this);
mFab = (FloatingActionsMenu) view.findViewById(R.id.fab_main);
@@ -170,13 +175,12 @@ public class KeyListFragment extends LoaderFragment
// show app name instead of "keys" from nav drawer
final FragmentActivity activity = getActivity();
activity.setTitle(R.string.app_name);
mStickyList.setOnItemClickListener(this);
mStickyList.setAreHeadersSticky(true);
mStickyList.setDrawingListUnderStickyHeader(false);
mStickyList.setFastScrollEnabled(true);
//mStickyList.setOnItemClickListener(this);
//mStickyList.setAreHeadersSticky(true);
//mStickyList.setDrawingListUnderStickyHeader(false);
//mStickyList.setFastScrollEnabled(true);
// Adds an empty footer view so that the Floating Action Button won't block content
// in last few rows.
@@ -192,14 +196,16 @@ public class KeyListFragment extends LoaderFragment
);
footer.setLayoutParams(params);
mStickyList.addFooterView(footer, null, false);
//mStickyList.addFooterView(footer, null, false);
/*
* Multi-selection
*/
mStickyList.setFastScrollAlwaysVisible(true);
//mStickyList.setFastScrollAlwaysVisible(true);
mStickyList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
//mStickyList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
/*
mStickyList.getWrappedList().setMultiChoiceModeListener(new MultiChoiceModeListener() {
@Override
@@ -257,6 +263,7 @@ public class KeyListFragment extends LoaderFragment
}
});
*/
// We have a menu item to show in action bar.
setHasOptionsMenu(true);
@@ -265,7 +272,7 @@ public class KeyListFragment extends LoaderFragment
setContentShown(false);
// this view is made visible if no data is available
mStickyList.setEmptyView(activity.findViewById(R.id.key_list_empty));
// mStickyList.setEmptyView(activity.findViewById(R.id.key_list_empty));
// click on search button (in empty view) starts query for search string
vSearchContainer = (ViewAnimator) activity.findViewById(R.id.search_container);
@@ -278,8 +285,11 @@ public class KeyListFragment extends LoaderFragment
});
// Create an empty adapter we will use to display the loaded data.
mAdapter = new KeyListAdapter(activity, null, 0);
//mAdapter = new KeyListAdapter(activity, null, 0);
mAdapter = new KeySectionedListAdapter(getContext(), null);
mStickyList.setAdapter(mAdapter);
mStickyList.setLayoutManager(new LayoutManager(getActivity()));
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.

View File

@@ -0,0 +1,285 @@
package org.sufficientlysecure.keychain.ui.util.adapter;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Handler;
import android.support.v7.widget.RecyclerView;
import org.sufficientlysecure.keychain.util.Log;
public abstract class CursorAdapter extends RecyclerView.Adapter {
public static final String TAG = "CursorAdapter";
private Cursor mCursor;
private Context mContext;
private boolean mDataValid;
private ChangeObserver mChangeObserver;
private DataSetObserver mDataSetObserver;
/**
* If set the adapter will register a content observer on the cursor and will call
* {@link #onContentChanged()} when a notification comes in. Be careful when
* using this flag: you will need to unset the current Cursor from the adapter
* to avoid leaks due to its registered observers. This flag is not needed
* when using a CursorAdapter with a
* {@link android.content.CursorLoader}.
*/
public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
/**
* Constructor that allows control over auto-requery. It is recommended
* you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}.
* When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
* will always be set.
*
* @param c The cursor from which to get the data.
* @param context The context
*/
public CursorAdapter(Context context, Cursor c) {
init(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
}
/**
* Recommended constructor.
*
* @param c The cursor from which to get the data.
* @param context The context
* @param flags Flags used to determine the behavior of the adapter
* @see #FLAG_REGISTER_CONTENT_OBSERVER
*/
public CursorAdapter(Context context, Cursor c, int flags) {
init(context, c, flags);
}
private void init(Context context, Cursor c, int flags) {
boolean cursorPresent = c != null;
mCursor = c;
mDataValid = cursorPresent;
mContext = context;
if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
mChangeObserver = new ChangeObserver();
mDataSetObserver = new MyDataSetObserver();
} else {
mChangeObserver = null;
mDataSetObserver = null;
}
if (cursorPresent) {
if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
}
setHasStableIds(true);
}
/**
* Returns the cursor.
* @return the cursor.
*/
public Cursor getCursor() {
return mCursor;
}
public Context getContext() {
return mContext;
}
/**
* @see android.support.v7.widget.RecyclerView.Adapter#getItemCount()
*/
@Override
public int getItemCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
} else {
return 0;
}
}
public boolean hasValidData() {
mDataValid = hasOpenCursor();
return mDataValid;
}
private boolean hasOpenCursor() {
Cursor cursor = getCursor();
if (cursor != null && cursor.isClosed()) {
swapCursor(null);
return false;
}
return cursor != null;
}
/**
* @see android.support.v7.widget.RecyclerView.Adapter#getItemId(int)
*
* @param position Adapter position to query
* @return the id of the item
*/
@Override
public long getItemId(int position) {
if (mDataValid && mCursor != null) {
if (moveCursor(position)) {
return getIdFromCursor(mCursor);
} else {
return RecyclerView.NO_ID;
}
} else {
return RecyclerView.NO_ID;
}
}
/**
* Return the id of the item represented by the row the cursor
* is currently moved to.
* @param cursor The cursor moved to the correct position.
* @return The id of the dataset
*/
public long getIdFromCursor(Cursor cursor) {
if(cursor != null) {
return cursor.getPosition();
} else {
return RecyclerView.NO_ID;
}
}
public void moveCursorOrThrow(int position)
throws IndexOutOfBoundsException, IllegalStateException {
if(position >= getItemCount() || position < -1) {
throw new IndexOutOfBoundsException("Position: " + position
+ " is invalid for this data set!");
}
if(!mDataValid) {
throw new IllegalStateException("Attempt to move cursor over invalid data set!");
}
if(!mCursor.moveToPosition(position)) {
throw new IllegalStateException("Couldn't move cursor from position: "
+ mCursor.getPosition() + " to position: " + position + "!");
}
}
public boolean moveCursor(int position) {
if(position >= getItemCount() || position < -1) {
Log.w(TAG, "Position: %d is invalid for this data set!");
return false;
}
if(!mDataValid) {
Log.d(TAG, "Attempt to move cursor over invalid data set!");
}
return mCursor.moveToPosition(position);
}
/**
* Change the underlying cursor to a new cursor. If there is an existing cursor it will be
* closed.
*
* @param cursor The new cursor to be used
*/
public void changeCursor(Cursor cursor) {
Cursor old = swapCursor(cursor);
if (old != null) {
old.close();
}
}
/**
* Swap in a new Cursor, returning the old Cursor. Unlike
* {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
* closed.
*
* @param newCursor The new cursor to be used.
* @return Returns the previously set Cursor, or null if there wasa not one.
* If the given new Cursor is the same instance is the previously set
* Cursor, null is also returned.
*/
public Cursor swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
Cursor oldCursor = mCursor;
if (oldCursor != null) {
if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
}
mCursor = newCursor;
if (newCursor != null) {
if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
mDataValid = true;
// notify the observers about the new cursor
onContentChanged();
} else {
mDataValid = false;
// notify the observers about the lack of a data set
onContentChanged();
}
return oldCursor;
}
/**
* <p>Converts the cursor into a CharSequence. Subclasses should override this
* method to convert their results. The default implementation returns an
* empty String for null values or the default String representation of
* the value.</p>
*
* @param cursor the cursor to convert to a CharSequence
* @return a CharSequence representing the value
*/
public CharSequence convertToString(Cursor cursor) {
return cursor == null ? "" : cursor.toString();
}
/**
* Called when the {@link ContentObserver} on the cursor receives a change notification.
* The default implementation provides the auto-requery logic, but may be overridden by
* sub classes.
*
* @see ContentObserver#onChange(boolean)
*/
protected void onContentChanged() {
notifyDataSetChanged();
}
private class ChangeObserver extends ContentObserver {
public ChangeObserver() {
super(new Handler());
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
onContentChanged();
}
}
private class MyDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
mDataValid = true;
onContentChanged();
}
@Override
public void onInvalidated() {
mDataValid = false;
onContentChanged();
}
}
}

View File

@@ -0,0 +1,104 @@
package org.sufficientlysecure.keychain.ui.util.adapter;
import android.database.Cursor;
import org.openintents.openpgp.util.OpenPgpUtils;
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKey;
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
import org.sufficientlysecure.keychain.pgp.KeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import java.io.Serializable;
import java.util.Date;
public interface KeyAdapter {
// These are the rows that we will retrieve.
String[] PROJECTION = new String[] {
KeychainContract.KeyRings._ID,
KeychainContract.KeyRings.MASTER_KEY_ID,
KeychainContract.KeyRings.USER_ID,
KeychainContract.KeyRings.IS_REVOKED,
KeychainContract.KeyRings.IS_EXPIRED,
KeychainContract.KeyRings.VERIFIED,
KeychainContract.KeyRings.HAS_ANY_SECRET,
KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID,
KeychainContract.KeyRings.FINGERPRINT,
KeychainContract.KeyRings.CREATION,
KeychainContract.KeyRings.HAS_ENCRYPT
};
// projection indices
int INDEX_MASTER_KEY_ID = 1;
int INDEX_USER_ID = 2;
int INDEX_IS_REVOKED = 3;
int INDEX_IS_EXPIRED = 4;
int INDEX_VERIFIED = 5;
int INDEX_HAS_ANY_SECRET = 6;
int INDEX_HAS_DUPLICATE_USER_ID = 7;
int INDEX_FINGERPRINT = 8;
int INDEX_CREATION = 9;
int INDEX_HAS_ENCRYPT = 10;
// adapter functionality
void setSearchQuery(String query);
boolean isEnabled(Cursor cursor);
KeyItem getItem(int position);
long getMasterKeyId(int position);
boolean isSecretAvailable(int position);
class KeyItem implements Serializable {
final String mUserIdFull;
final OpenPgpUtils.UserId mUserId;
final long mKeyId;
final boolean mHasDuplicate;
final boolean mHasEncrypt;
final Date mCreation;
final String mFingerprint;
final boolean mIsSecret, mIsRevoked, mIsExpired, mIsVerified;
public KeyItem(Cursor cursor) {
String userId = cursor.getString(INDEX_USER_ID);
mUserId = KeyRing.splitUserId(userId);
mUserIdFull = userId;
mKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
mHasDuplicate = cursor.getLong(INDEX_HAS_DUPLICATE_USER_ID) > 0;
mHasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0;
mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000);
mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
cursor.getBlob(INDEX_FINGERPRINT));
mIsSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0;
mIsRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0;
mIsExpired = cursor.getInt(INDEX_IS_EXPIRED) > 0;
mIsVerified = cursor.getInt(INDEX_VERIFIED) > 0;
}
public KeyItem(CanonicalizedPublicKeyRing ring) {
CanonicalizedPublicKey key = ring.getPublicKey();
String userId = key.getPrimaryUserIdWithFallback();
mUserId = KeyRing.splitUserId(userId);
mUserIdFull = userId;
mKeyId = ring.getMasterKeyId();
mHasDuplicate = false;
mHasEncrypt = key.getKeyRing().getEncryptIds().size() > 0;
mCreation = key.getCreationTime();
mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
ring.getFingerprint());
mIsRevoked = key.isRevoked();
mIsExpired = key.isExpired();
// these two are actually "don't know"s
mIsSecret = false;
mIsVerified = false;
}
public String getReadableName() {
if (mUserId.name != null) {
return mUserId.name;
} else {
return mUserId.email;
}
}
}
}

View File

@@ -0,0 +1,268 @@
package org.sufficientlysecure.keychain.ui.util.adapter;
import android.content.Context;
import android.database.Cursor;
import android.graphics.PorterDuff;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
import android.util.SparseBooleanArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import org.openintents.openpgp.util.OpenPgpUtils;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.Highlighter;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
public class KeySectionedListAdapter extends SectionCursorAdapter<String, KeySectionedListAdapter.KeyItemViewHolder, KeySectionedListAdapter.KeyHeaderViewHolder> implements KeyAdapter {
private String mQuery;
private SparseBooleanArray mSelectionMap;
public KeySectionedListAdapter(Context context, Cursor cursor) {
super(context, cursor, 0);
mQuery = "";
mSelectionMap = new SparseBooleanArray();
}
@Override
public void setSearchQuery(String query) {
mQuery = query;
}
@Override
public boolean isEnabled(Cursor cursor) {
return true;
}
@Override
public KeyItem getItem(int position) {
Cursor cursor = getCursor();
if(cursor != null) {
if(cursor.getPosition() != position) {
moveCursor(position);
}
return new KeyItem(cursor);
}
return null;
}
@Override
public long getMasterKeyId(int position) {
return 0;
}
@Override
public boolean isSecretAvailable(int position) {
return false;
}
@Override
protected String getSectionFromCursor(Cursor cursor) throws IllegalStateException {
if (cursor.getInt(INDEX_HAS_ANY_SECRET) != 0) {
return getContext().getString(R.string.my_keys);
}
String userId = cursor.getString(INDEX_USER_ID);
String headerText = getContext().getString(R.string.user_id_no_name);
if (userId != null && userId.length() > 0) {
headerText = "" + userId.charAt(0);
}
return headerText;
}
@Override
protected KeyHeaderViewHolder onCreateSectionViewHolder(ViewGroup parent) {
return new KeyHeaderViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.key_list_header, parent, false));
}
@Override
protected KeyItemViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
return new KeyItemViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.key_list_item, parent, false));
}
@Override
protected void onBindSectionViewHolder(KeyHeaderViewHolder holder, int sectionIndex, String section) {
System.out.println("SIX: " + sectionIndex);
if(sectionIndex == 0) {
holder.bind(section, getCursor().getCount());
} else {
holder.bind(section);
}
}
@Override
protected void onBindItemViewHolder(KeyItemViewHolder holder, Cursor cursor) {
boolean isSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0;
long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
if (isSecret && masterKeyId == 0L) {
holder.bindDummy();
} else {
Highlighter highlighter = new Highlighter(getContext(), mQuery);
holder.bindKey(new KeyItem(cursor), highlighter);
}
}
static class KeyItemViewHolder extends RecyclerView.ViewHolder {
private View mLayoutDummy;
private View mLayoutData;
private Long mMasterKeyId;
private TextView mMainUserId;
private TextView mMainUserIdRest;
private TextView mCreationDate;
private ImageView mStatus;
private View mSlinger;
private ImageButton mSlingerButton;
public KeyItemViewHolder(View itemView) {
super(itemView);
mLayoutData = itemView.findViewById(R.id.key_list_item_data);
mLayoutDummy = itemView.findViewById(R.id.key_list_item_dummy);
mMainUserId = (TextView) itemView.findViewById(R.id.key_list_item_name);
mMainUserIdRest = (TextView) itemView.findViewById(R.id.key_list_item_email);
mStatus = (ImageView) itemView.findViewById(R.id.key_list_item_status_icon);
mSlinger = itemView.findViewById(R.id.key_list_item_slinger_view);
mSlingerButton = (ImageButton) itemView.findViewById(R.id.key_list_item_slinger_button);
mCreationDate = (TextView) itemView.findViewById(R.id.key_list_item_creation);
}
public void bindKey(KeyItem keyItem, Highlighter highlighter) {
Context ctx = itemView.getContext();
mLayoutData.setVisibility(View.VISIBLE);
mLayoutDummy.setVisibility(View.GONE);
{ // set name and stuff, common to both key types
OpenPgpUtils.UserId userIdSplit = keyItem.mUserId;
if (userIdSplit.name != null) {
mMainUserId.setText(highlighter.highlight(userIdSplit.name));
} else {
mMainUserId.setText(R.string.user_id_no_name);
}
if (userIdSplit.email != null) {
mMainUserIdRest.setText(highlighter.highlight(userIdSplit.email));
mMainUserIdRest.setVisibility(View.VISIBLE);
} else {
mMainUserIdRest.setVisibility(View.GONE);
}
}
// sort of a hack: if this item isn't enabled, we make it clickable
// to intercept its click events. either way, no listener!
itemView.setClickable(false);
{ // set edit button and status, specific by key type
mMasterKeyId = keyItem.mKeyId;
int textColor;
// Note: order is important!
if (keyItem.mIsRevoked) {
KeyFormattingUtils
.setStatusImage(ctx, mStatus, null, KeyFormattingUtils.State.REVOKED, R.color.key_flag_gray);
mStatus.setVisibility(View.VISIBLE);
mSlinger.setVisibility(View.GONE);
textColor = ctx.getResources().getColor(R.color.key_flag_gray);
} else if (keyItem.mIsExpired) {
KeyFormattingUtils.setStatusImage(ctx, mStatus, null, KeyFormattingUtils.State.EXPIRED, R.color.key_flag_gray);
mStatus.setVisibility(View.VISIBLE);
mSlinger.setVisibility(View.GONE);
textColor = ctx.getResources().getColor(R.color.key_flag_gray);
} else if (keyItem.mIsSecret) {
mStatus.setVisibility(View.GONE);
if (mSlingerButton.hasOnClickListeners()) {
mSlingerButton.setColorFilter(
FormattingUtils.getColorFromAttr(ctx, R.attr.colorTertiaryText),
PorterDuff.Mode.SRC_IN);
mSlinger.setVisibility(View.VISIBLE);
} else {
mSlinger.setVisibility(View.GONE);
}
textColor = FormattingUtils.getColorFromAttr(ctx, R.attr.colorText);
} else {
// this is a public key - show if it's verified
if (keyItem.mIsVerified) {
KeyFormattingUtils.setStatusImage(ctx, mStatus, KeyFormattingUtils.State.VERIFIED);
mStatus.setVisibility(View.VISIBLE);
} else {
KeyFormattingUtils.setStatusImage(ctx, mStatus, KeyFormattingUtils.State.UNVERIFIED);
mStatus.setVisibility(View.VISIBLE);
}
mSlinger.setVisibility(View.GONE);
textColor = FormattingUtils.getColorFromAttr(ctx, R.attr.colorText);
}
mMainUserId.setTextColor(textColor);
mMainUserIdRest.setTextColor(textColor);
if (keyItem.mHasDuplicate) {
String dateTime = DateUtils.formatDateTime(ctx,
keyItem.mCreation.getTime(),
DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR
| DateUtils.FORMAT_ABBREV_MONTH);
mCreationDate.setText(ctx.getString(R.string.label_key_created,
dateTime));
mCreationDate.setTextColor(textColor);
mCreationDate.setVisibility(View.VISIBLE);
} else {
mCreationDate.setVisibility(View.GONE);
}
}
}
public void bindDummy() {
// just reset everything to display the dummy layout
mLayoutDummy.setVisibility(View.VISIBLE);
mLayoutData.setVisibility(View.GONE);
mSlinger.setVisibility(View.GONE);
mStatus.setVisibility(View.GONE);
itemView.setClickable(false);
}
}
static class KeyHeaderViewHolder extends RecyclerView.ViewHolder {
private TextView mHeaderText;
private TextView mHeaderCount;
public KeyHeaderViewHolder(View itemView) {
super(itemView);
mHeaderText = (TextView) itemView.findViewById(R.id.stickylist_header_text);
mHeaderCount = (TextView) itemView.findViewById(R.id.contacts_num);
}
public void bind(String title, int count) {
mHeaderText.setText(title);
String contactsTotal = itemView.getResources()
.getQuantityString(R.plurals.n_keys, count, count);
mHeaderCount.setText(contactsTotal);
mHeaderCount.setVisibility(View.VISIBLE);
}
public void bind(String title) {
mHeaderText.setText(title);
mHeaderCount.setVisibility(View.GONE);
}
}
}

View File

@@ -0,0 +1,351 @@
package org.sufficientlysecure.keychain.ui.util.adapter;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.util.SparseArrayCompat;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import android.widget.SectionIndexer;
import com.tonicartos.superslim.GridSLM;
import com.tonicartos.superslim.LayoutManager;
import com.tonicartos.superslim.LinearSLM;
import org.sufficientlysecure.keychain.util.Log;
import java.lang.annotation.Inherited;
import java.util.ArrayList;
import java.util.List;
/**
* @param <T> section type.
* @param <VH> the view holder extending {@code BaseViewHolder<Cursor>} that is bound to the cursor data.
* @param <SH> the view holder extending {@code BaseViewHolder<<T>>} that is bound to the section data.
*/
public abstract class SectionCursorAdapter<T, VH extends RecyclerView.ViewHolder, SH extends RecyclerView.ViewHolder>
extends CursorAdapter implements SectionIndexer {
public static final String TAG = "SectionCursorAdapter";
private static final int VIEW_TYPE_ITEM = 0x0;
private static final int VIEW_TYPE_SECTION = 0x1;
private SparseArrayCompat<T> mSectionMap = new SparseArrayCompat<>();
private ArrayList<Integer> mSectionIndexList = new ArrayList<>();
private Comparator<T> mSectionComparator;
private Object[] mFastScrollItems;
public SectionCursorAdapter(Context context, Cursor cursor, int flags) {
this(context, cursor, flags, new Comparator<T>() {
@Override
public boolean equal(T obj1, T obj2) {
return (obj1 == null) ?
obj2 == null : obj1.equals(obj2);
}
});
}
public SectionCursorAdapter(Context context, Cursor cursor, int flags, Comparator<T> comparator) {
super(context, cursor, flags);
setSectionComparator(comparator);
}
@Override
public void onContentChanged() {
if (hasValidData()) {
buildSections();
} else {
mSectionMap.clear();
mSectionIndexList.clear();
mFastScrollItems = null;
}
super.onContentChanged();
}
/**
* Assign a comparator which will be used to check whether
* a section is contained in the list of sections. The default implementation
* will check for null pointers and compare sections using the {@link #equals(Object)} method.
* @param comparator The comparator to compare section objects.
*/
public void setSectionComparator(Comparator<T> comparator) {
this.mSectionComparator = comparator;
buildSections();
}
/**
* If the adapter's cursor is not null then this method will call buildSections(Cursor cursor).
*/
private void buildSections() {
if (hasValidData()) {
moveCursor(-1);
try {
mSectionMap.clear();
mSectionIndexList.clear();
mFastScrollItems = null;
appendSections(getCursor());
} catch (IllegalStateException e) {
Log.e(TAG, "Couldn't build sections. Perhaps you're moving the cursor" +
"in #getSectionFromCursor(Cursor)?", e);
swapCursor(null);
mSectionMap.clear();
mSectionIndexList.clear();
mFastScrollItems = null;
}
}
}
protected void appendSections(Cursor cursor) throws IllegalStateException {
int cursorPosition = 0;
while(hasValidData() && cursor.moveToNext()) {
T section = getSectionFromCursor(cursor);
if (cursor.getPosition() != cursorPosition)
throw new IllegalStateException("Do not move the cursor's position in getSectionFromCursor.");
if (!hasSection(section))
mSectionMap.append(cursorPosition + mSectionMap.size(), section);
cursorPosition++;
}
}
public boolean hasSection(T section) {
for(int i = 0; i < mSectionMap.size(); i++) {
T obj = mSectionMap.valueAt(i);
if(mSectionComparator.equal(obj, section))
return true;
}
return false;
}
/**
* The object which is return will determine what section this cursor position will be in.
* @return the section from the cursor at its current position.
* This object will be passed to newSectionView and bindSectionView.
*/
protected abstract T getSectionFromCursor(Cursor cursor) throws IllegalStateException;
protected String getTitleFromSection(T section) {
return section != null ? section.toString() : "";
}
@Override
public int getItemCount() {
return super.getItemCount() + mSectionMap.size();
}
@Override
public final long getItemId(int listPosition) {
if (isSection(listPosition))
return listPosition;
else {
int cursorPosition = getCursorPositionWithoutSections(listPosition);
return super.getItemId(cursorPosition);
}
}
/**
* @param listPosition the position of the current item in the list with mSectionMap included
* @return Whether or not the listPosition points to a section.
*/
public boolean isSection(int listPosition) {
return mSectionMap.indexOfKey(listPosition) >= 0;
}
/**
* This will map a position in the list adapter (which includes mSectionMap) to a position in
* the cursor (which does not contain mSectionMap).
*
* @param listPosition the position of the current item in the list with mSectionMap included
* @return the correct position to use with the cursor
*/
public int getCursorPositionWithoutSections(int listPosition) {
if (mSectionMap.size() == 0) {
return listPosition;
} else if (!isSection(listPosition)) {
int sectionIndex = getSectionForPosition(listPosition);
if (isListPositionBeforeFirstSection(listPosition, sectionIndex)) {
return listPosition;
} else {
return listPosition - (sectionIndex + 1);
}
} else {
return -1;
}
}
/**
* Get the section object for the index within the array of sections.
* @param sectionPosition The section index.
* @return The specified section object for this position.
*/
public T getSection(int sectionPosition) {
if (mSectionIndexList.contains(sectionPosition)) {
return mSectionMap.get(mSectionIndexList.get(sectionPosition));
}
return null;
}
/**
* Returns all indices at which the first item of a section is placed.
* @return The first index of each section.
*/
public List<Integer> getSectionListPositions() {
return mSectionIndexList;
}
/**
* {@inheritDoc}
*/
@Override
public int getPositionForSection(int sectionIndex) {
if (mSectionIndexList.isEmpty()) {
for (int i = 0; i < mSectionMap.size(); i++) {
mSectionIndexList.add(mSectionMap.keyAt(i));
}
}
return sectionIndex < mSectionIndexList.size() ?
mSectionIndexList.get(sectionIndex) : getItemCount();
}
/**
* Given the position of a section, returns its index in the array of sections.
* @param sectionPosition The first position of the corresponding section in the array of items.
* @return The section index in the array of sections.
*/
public int getSectionIndexForPosition(int sectionPosition) {
if (mSectionIndexList.isEmpty()) {
for (int i = 0; i < mSectionMap.size(); i++) {
mSectionIndexList.add(mSectionMap.keyAt(i));
}
}
return mSectionIndexList.indexOf(sectionPosition);
}
/**
* Given the list position of an item in the adapter, returns the
* adapter position of the first item of the section the given item belongs to.
* @param listPosition The absolute list position.
* @return The position of the first item of the section.
*/
public int getFirstSectionPosition(int listPosition) {
int start = 0;
for(int i = 0; i <= listPosition; i++) {
if(isSection(i)) {
start = i;
}
}
return start;
}
/**
* {@inheritDoc}
*/
@Override
public int getSectionForPosition(int listPosition) {
boolean isSection = false;
int numPrecedingSections = 0;
for (int i = 0; i < mSectionMap.size(); i++) {
int sectionPosition = mSectionMap.keyAt(i);
if (listPosition > sectionPosition) {
numPrecedingSections++;
} else if (listPosition == sectionPosition) {
isSection = true;
} else {
break;
}
}
return isSection ? numPrecedingSections : Math.max(numPrecedingSections - 1, 0);
}
@Override
public Object[] getSections() {
if(mFastScrollItems == null) {
mFastScrollItems = getSectionLabels();
}
return mFastScrollItems;
}
private Object[] getSectionLabels() {
if(mSectionMap == null)
return new Object[0];
String[] ret = new String[mSectionMap.size()];
for(int i = 0; i < ret.length; i++) {
ret[i] = getTitleFromSection(mSectionMap.valueAt(i));
}
return ret;
}
private boolean isListPositionBeforeFirstSection(int listPosition, int sectionIndex) {
boolean hasSections = mSectionMap != null && mSectionMap.size() > 0;
return sectionIndex == 0 && hasSections && listPosition < mSectionMap.keyAt(0);
}
@Override
public final int getItemViewType(int listPosition) {
return isSection(listPosition) ? VIEW_TYPE_SECTION : VIEW_TYPE_ITEM;
}
@Override
@SuppressWarnings("unchecked")
public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
LayoutManager.LayoutParams layoutParams = LayoutManager.LayoutParams
.from(holder.itemView.getLayoutParams());
// assign first position of section to each item
layoutParams.setFirstPosition(getFirstSectionPosition(position));
switch (holder.getItemViewType()) {
case VIEW_TYPE_ITEM :
moveCursorOrThrow(getCursorPositionWithoutSections(position));
onBindItemViewHolder((VH) holder, getCursor());
layoutParams.isHeader = false;
break;
case VIEW_TYPE_SECTION:
T section = mSectionMap.get(position);
int sectionIndex = getSectionIndexForPosition(position);
onBindSectionViewHolder((SH) holder, sectionIndex, section);
layoutParams.isHeader = true;
break;
}
holder.itemView.setLayoutParams(layoutParams);
}
@Override
public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_SECTION:
return onCreateSectionViewHolder(parent);
case VIEW_TYPE_ITEM:
return onCreateItemViewHolder(parent, viewType);
default:
return null;
}
}
protected abstract SH onCreateSectionViewHolder(ViewGroup parent);
protected abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType);
protected abstract void onBindSectionViewHolder(SH holder, int sectionIndex, T section);
protected abstract void onBindItemViewHolder(VH holder, Cursor cursor);
public interface Comparator<T> {
boolean equal(T obj1, T obj2);
}
}

View File

@@ -12,12 +12,10 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<se.emilsjolander.stickylistheaders.StickyListHeadersListView
<android.support.v7.widget.RecyclerView
android:id="@+id/key_list_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawSelectorOnTop="true"
android:fastScrollEnabled="true"
android:paddingLeft="16dp"
android:paddingRight="32dp"
android:scrollbarStyle="outsideOverlay" />

View File

@@ -1,7 +1,16 @@
<?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:super="http://schemas.android.com/apk/lib-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
android:layout_height="wrap_content"
android:background="?android:colorBackground"
super:slm_headerDisplay="sticky|inline"
super:slm_section_sectionManager="linear"
tools:ignore="ResAuto">
<TextView
style="@style/SectionHeader"