From d5321a6fb2c775787a4d1a9bb012b042048ed6c3 Mon Sep 17 00:00:00 2001 From: Tobias Erthal Date: Fri, 2 Sep 2016 22:41:45 +0200 Subject: [PATCH] First try implementing sticky section headers --- OpenKeychain/build.gradle | 1 + .../keychain/ui/KeyListFragment.java | 38 +- .../ui/util/adapter/CursorAdapter.java | 285 ++++++++++++++ .../keychain/ui/util/adapter/KeyAdapter.java | 104 ++++++ .../util/adapter/KeySectionedListAdapter.java | 268 +++++++++++++ .../ui/util/adapter/SectionCursorAdapter.java | 351 ++++++++++++++++++ .../src/main/res/layout/key_list_fragment.xml | 4 +- .../src/main/res/layout/key_list_header.xml | 13 +- 8 files changed, 1045 insertions(+), 19 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeyAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeySectionedListAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java 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">