diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle
index e3919fae6..2fe402a25 100644
--- a/OpenKeychain/build.gradle
+++ b/OpenKeychain/build.gradle
@@ -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'
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
index 77139f5de..4ee821322 100644
--- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
@@ -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.
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java
new file mode 100644
index 000000000..57315ea5e
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java
@@ -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 not
+ * 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;
+ }
+
+ /**
+ *
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.
+ *
+ * @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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeyAdapter.java
new file mode 100644
index 000000000..90f8ee482
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeyAdapter.java
@@ -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;
+ }
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeySectionedListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeySectionedListAdapter.java
new file mode 100644
index 000000000..673f76928
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeySectionedListAdapter.java
@@ -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 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);
+ }
+ }
+}
diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java
new file mode 100644
index 000000000..b2175b5c7
--- /dev/null
+++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java
@@ -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 section type.
+ * @param the view holder extending {@code BaseViewHolder} that is bound to the cursor data.
+ * @param the view holder extending {@code BaseViewHolder<>} that is bound to the section data.
+ */
+public abstract class SectionCursorAdapter
+ 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 mSectionMap = new SparseArrayCompat<>();
+ private ArrayList mSectionIndexList = new ArrayList<>();
+ private Comparator mSectionComparator;
+ private Object[] mFastScrollItems;
+
+ public SectionCursorAdapter(Context context, Cursor cursor, int flags) {
+ this(context, cursor, flags, new Comparator() {
+ @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 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 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 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 {
+ boolean equal(T obj1, T obj2);
+ }
+}
+
diff --git a/OpenKeychain/src/main/res/layout/key_list_fragment.xml b/OpenKeychain/src/main/res/layout/key_list_fragment.xml
index 6aaf5be25..ce40dac8c 100644
--- a/OpenKeychain/src/main/res/layout/key_list_fragment.xml
+++ b/OpenKeychain/src/main/res/layout/key_list_fragment.xml
@@ -12,12 +12,10 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
diff --git a/OpenKeychain/src/main/res/layout/key_list_header.xml b/OpenKeychain/src/main/res/layout/key_list_header.xml
index 4809fc5ab..fbe9c84c4 100644
--- a/OpenKeychain/src/main/res/layout/key_list_header.xml
+++ b/OpenKeychain/src/main/res/layout/key_list_header.xml
@@ -1,7 +1,16 @@
-
+ android:layout_height="wrap_content"
+ android:background="?android:colorBackground"
+
+ super:slm_headerDisplay="sticky|inline"
+ super:slm_section_sectionManager="linear"
+ tools:ignore="ResAuto">