From d5321a6fb2c775787a4d1a9bb012b042048ed6c3 Mon Sep 17 00:00:00 2001 From: Tobias Erthal Date: Fri, 2 Sep 2016 22:41:45 +0200 Subject: [PATCH 01/17] 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"> Date: Tue, 6 Sep 2016 01:40:16 +0200 Subject: [PATCH 02/17] Improved SectionedCursorAdapter, added support for the dummy key item view. --- .../keychain/ui/KeyListFragment.java | 83 ++-- .../keychain/ui/adapter/KeyAdapter.java | 23 - .../adapter/KeySectionedListAdapter.java | 238 +++++++---- .../keychain/ui/util/adapter/KeyAdapter.java | 16 +- .../ui/util/adapter/SectionCursorAdapter.java | 158 +++---- .../util/recyclerview/RecyclerFragment.java | 397 ++++++++++++++++++ .../src/main/res/layout/key_list_dummy.xml | 47 +++ .../src/main/res/layout/key_list_fragment.xml | 209 +++++---- ...header.xml => key_list_header_private.xml} | 14 +- .../res/layout/key_list_header_public.xml | 22 + .../src/main/res/layout/key_list_item.xml | 64 +-- 11 files changed, 845 insertions(+), 426 deletions(-) rename OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/{util => }/adapter/KeySectionedListAdapter.java (56%) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerFragment.java create mode 100644 OpenKeychain/src/main/res/layout/key_list_dummy.xml rename OpenKeychain/src/main/res/layout/{key_list_header.xml => key_list_header_private.xml} (76%) create mode 100644 OpenKeychain/src/main/res/layout/key_list_header_public.xml 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 4ee821322..4ed45832d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -21,12 +21,10 @@ package org.sufficientlysecure.keychain.ui; import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; -import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -35,7 +33,6 @@ 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; @@ -46,11 +43,8 @@ import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.AdapterView; import android.widget.Button; -import android.widget.ListView; -import android.widget.TextView; import android.widget.ViewAnimator; import com.getbase.floatingactionbutton.FloatingActionButton; @@ -73,26 +67,22 @@ import org.sufficientlysecure.keychain.service.ConsolidateInputParcel; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; -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.ui.adapter.KeySectionedListAdapter; +import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerFragment; import org.sufficientlysecure.keychain.util.FabContainer; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.Preferences; -import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; -import se.emilsjolander.stickylistheaders.StickyListHeadersListView; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; /** * Public key list with sticky list headers. It does _not_ extend ListFragment because it uses * StickyListHeaders library which does not extend upon ListView. */ -public class KeyListFragment extends LoaderFragment +public class KeyListFragment extends RecyclerFragment implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks, FabContainer, CryptoOperationHelper.Callback { @@ -101,10 +91,6 @@ public class KeyListFragment extends LoaderFragment private static final int REQUEST_DELETE = 2; private static final int REQUEST_VIEW_KEY = 3; - //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; @@ -126,12 +112,8 @@ public class KeyListFragment extends LoaderFragment * Load custom layout with StickyListView from library */ @Override - public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) { - View root = super.onCreateView(inflater, superContainer, savedInstanceState); - View view = inflater.inflate(R.layout.key_list_fragment, getContainer()); - - mStickyList = (RecyclerView) view.findViewById(R.id.key_list_list); - //mStickyList.setOnItemClickListener(this); + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.key_list_fragment, container, false); mFab = (FloatingActionsMenu) view.findViewById(R.id.fab_main); @@ -162,7 +144,7 @@ public class KeyListFragment extends LoaderFragment }); - return root; + return view; } /** @@ -182,7 +164,7 @@ public class KeyListFragment extends LoaderFragment //mStickyList.setDrawingListUnderStickyHeader(false); //mStickyList.setFastScrollEnabled(true); - // Adds an empty footer view so that the Floating Action Button won't block content + /* Adds an empty footer view so that the Floating Action Button won't block content // in last few rows. View footer = new View(activity); @@ -197,6 +179,7 @@ public class KeyListFragment extends LoaderFragment footer.setLayoutParams(params); //mStickyList.addFooterView(footer, null, false); + */ /* * Multi-selection @@ -269,7 +252,7 @@ public class KeyListFragment extends LoaderFragment setHasOptionsMenu(true); // Start out with a progress indicator. - setContentShown(false); + hideList(true); // this view is made visible if no data is available // mStickyList.setEmptyView(activity.findViewById(R.id.key_list_empty)); @@ -286,10 +269,9 @@ 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 KeySectionedListAdapter(getContext(), null); - mStickyList.setAdapter(mAdapter); - mStickyList.setLayoutManager(new LayoutManager(getActivity())); + setAdapter(new KeySectionedListAdapter(getContext(), null)); + setLayoutManager(new LayoutManager(getActivity())); // Prepare the loader. Either re-connect with an existing one, // or start a new one. @@ -324,22 +306,22 @@ public class KeyListFragment extends LoaderFragment // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), uri, KeyListAdapter.PROJECTION, null, null, ORDER); + return new CursorLoader(getActivity(), uri, KeyAdapter.PROJECTION, null, null, ORDER); } @Override public void onLoadFinished(Loader loader, Cursor data) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) - mAdapter.setSearchQuery(mQuery); + getAdapter().setSearchQuery(mQuery); if (data != null && (mQuery == null || TextUtils.isEmpty(mQuery))) { - boolean isSecret = data.moveToFirst() && data.getInt(KeyListAdapter.INDEX_HAS_ANY_SECRET) != 0; + boolean isSecret = data.moveToFirst() && data.getInt(KeyAdapter.INDEX_HAS_ANY_SECRET) != 0; if (!isSecret) { - MatrixCursor headerCursor = new MatrixCursor(KeyListAdapter.PROJECTION); - Long[] row = new Long[KeyListAdapter.PROJECTION.length]; - row[KeyListAdapter.INDEX_HAS_ANY_SECRET] = 1L; - row[KeyListAdapter.INDEX_MASTER_KEY_ID] = 0L; + MatrixCursor headerCursor = new MatrixCursor(KeyAdapter.PROJECTION); + Long[] row = new Long[KeyAdapter.PROJECTION.length]; + row[KeyAdapter.INDEX_HAS_ANY_SECRET] = 1L; + row[KeyAdapter.INDEX_MASTER_KEY_ID] = 0L; headerCursor.addRow(row); Cursor dataCursor = data; @@ -348,7 +330,8 @@ public class KeyListFragment extends LoaderFragment }); } } - mAdapter.swapCursor(data); + + getAdapter().swapCursor(data); // end action mode, if any if (mActionMode != null) { @@ -357,9 +340,9 @@ public class KeyListFragment extends LoaderFragment // The list should now be shown. if (isResumed()) { - setContentShown(true); + showList(true); } else { - setContentShownNoAnimation(true); + showList(false); } } @@ -368,7 +351,7 @@ public class KeyListFragment extends LoaderFragment // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. - mAdapter.swapCursor(null); + getAdapter().swapCursor(null); } /** @@ -378,7 +361,7 @@ public class KeyListFragment extends LoaderFragment public void onItemClick(AdapterView adapterView, View view, int position, long id) { Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class); viewIntent.setData( - KeyRings.buildGenericKeyRingUri(mAdapter.getMasterKeyId(position))); + KeyRings.buildGenericKeyRingUri(getAdapter().getMasterKeyId(position))); startActivityForResult(viewIntent, REQUEST_VIEW_KEY); } @@ -762,6 +745,7 @@ public class KeyListFragment extends LoaderFragment return false; } + /* public class KeyListAdapter extends KeyAdapter implements StickyListHeadersAdapter { private HashMap mSelection = new HashMap<>(); @@ -842,19 +826,13 @@ public class KeyListFragment extends LoaderFragment TextView mCount; } - /** - * Creates a new header view and binds the section headers to it. It uses the ViewHolder - * pattern. Most functionality is similar to getView() from Android's CursorAdapter. - *

- * NOTE: The variables mDataValid and mCursor are available due to the super class - * CursorAdapter. - */ + @Override public View getHeaderView(int position, View convertView, ViewGroup parent) { HeaderViewHolder holder; if (convertView == null) { holder = new HeaderViewHolder(); - convertView = mInflater.inflate(R.layout.key_list_header, parent, false); + convertView = mInflater.inflate(R.layout.key_list_header_public, parent, false); holder.mText = (TextView) convertView.findViewById(R.id.stickylist_header_text); holder.mCount = (TextView) convertView.findViewById(R.id.contacts_num); convertView.setTag(holder); @@ -899,9 +877,6 @@ public class KeyListFragment extends LoaderFragment return convertView; } - /** - * Header IDs should be static, position=1 should always return the same Id that is. - */ @Override public long getHeaderId(int position) { if (!mDataValid) { @@ -927,9 +902,6 @@ public class KeyListFragment extends LoaderFragment } } - /** - * -------------------------- MULTI-SELECTION METHODS -------------- - */ public void setNewSelection(int position, boolean value) { mSelection.put(position, value); notifyDataSetChanged(); @@ -965,5 +937,6 @@ public class KeyListFragment extends LoaderFragment } } +*/ } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java index 56dd15a8f..cb02d4b6b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyAdapter.java @@ -94,7 +94,6 @@ public class KeyAdapter extends CursorAdapter { public static class KeyItemViewHolder { public View mView; - public View mLayoutDummy; public View mLayoutData; public Long mMasterKeyId; public TextView mMainUserId; @@ -109,7 +108,6 @@ public class KeyAdapter extends CursorAdapter { public KeyItemViewHolder(View view) { mView = view; mLayoutData = view.findViewById(R.id.key_list_item_data); - mLayoutDummy = view.findViewById(R.id.key_list_item_dummy); mMainUserId = (TextView) view.findViewById(R.id.key_list_item_name); mMainUserIdRest = (TextView) view.findViewById(R.id.key_list_item_email); mStatus = (ImageView) view.findViewById(R.id.key_list_item_status_icon); @@ -119,10 +117,6 @@ public class KeyAdapter extends CursorAdapter { } public void setData(Context context, KeyItem item, Highlighter highlighter, boolean enabled) { - - mLayoutData.setVisibility(View.VISIBLE); - mLayoutDummy.setVisibility(View.GONE); - mDisplayedItem = item; { // set name and stuff, common to both key types @@ -207,25 +201,8 @@ public class KeyAdapter extends CursorAdapter { } else { mCreationDate.setVisibility(View.GONE); } - } - } - - /** Shows the "you have no keys yet" dummy view, and sets an OnClickListener. */ - public void setDummy(OnClickListener listener) { - - // just reset everything to display the dummy layout - mLayoutDummy.setVisibility(View.VISIBLE); - mLayoutData.setVisibility(View.GONE); - mSlinger.setVisibility(View.GONE); - mStatus.setVisibility(View.GONE); - mView.setClickable(false); - - mLayoutDummy.setOnClickListener(listener); - - } - } public boolean isEnabled(Cursor cursor) { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeySectionedListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java similarity index 56% rename from OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeySectionedListAdapter.java rename to OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java index 673f76928..420e88214 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/KeySectionedListAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java @@ -1,9 +1,10 @@ -package org.sufficientlysecure.keychain.ui.util.adapter; +package org.sufficientlysecure.keychain.ui.adapter; import android.content.Context; import android.database.Cursor; import android.graphics.PorterDuff; -import android.support.v7.widget.RecyclerView; +import android.support.v4.content.ContextCompat; +import android.text.TextUtils; import android.text.format.DateUtils; import android.util.SparseBooleanArray; import android.view.LayoutInflater; @@ -14,15 +15,24 @@ import android.widget.ImageView; import android.widget.TextView; import org.openintents.openpgp.util.OpenPgpUtils; +import org.sufficientlysecure.keychain.Constants; 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; +import org.sufficientlysecure.keychain.ui.util.adapter.*; +import org.sufficientlysecure.keychain.util.Log; + +public class KeySectionedListAdapter extends SectionCursorAdapter implements org.sufficientlysecure.keychain.ui.util.adapter.KeyAdapter { + private static final short VIEW_ITEM_TYPE_KEY = 0x0; + private static final short VIEW_ITEM_TYPE_DUMMY = 0x1; + + private static final short VIEW_SECTION_TYPE_PRIVATE = 0x0; + private static final short VIEW_SECTION_TYPE_PUBLIC = 0x1; -public class KeySectionedListAdapter extends SectionCursorAdapter implements KeyAdapter { private String mQuery; private SparseBooleanArray mSelectionMap; - + private boolean mHasDummy = false; public KeySectionedListAdapter(Context context, Cursor cursor) { super(context, cursor, 0); @@ -67,57 +77,148 @@ public class KeySectionedListAdapter extends SectionCursorAdapter 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()); + return '#'; } else { - holder.bind(section); + String userId = cursor.getString(INDEX_USER_ID); + if(TextUtils.isEmpty(userId)) { + return '?'; + } else { + return Character.toUpperCase(userId.charAt(0)); + } } } @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(); + protected short getSectionHeaderViewType(int sectionIndex) { + return (sectionIndex < 1) ? + VIEW_SECTION_TYPE_PRIVATE : + VIEW_SECTION_TYPE_PUBLIC; + } + + @Override + protected short getSectionItemViewType(int position) { + if(moveCursor(position)) { + boolean hasMaster = getCursor().getLong(INDEX_MASTER_KEY_ID) != 0L; + boolean isSecret = getCursor().getInt(INDEX_HAS_ANY_SECRET) != 0; + + if (isSecret && !hasMaster) { + return VIEW_ITEM_TYPE_DUMMY; + } } else { + Log.w(Constants.TAG, "Unable to determine key view type. " + + "Reason: Could not move cursor over dataset."); + } + + return VIEW_ITEM_TYPE_KEY; + } + + @Override + protected KeyHeaderViewHolder onCreateSectionViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_SECTION_TYPE_PUBLIC: + return new KeyHeaderViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.key_list_header_public, parent, false)); + + case VIEW_SECTION_TYPE_PRIVATE: + return new KeyHeaderViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.key_list_header_private, parent, false)); + + default: + return null; + } + } + + @Override + protected ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + case VIEW_ITEM_TYPE_KEY: + return new KeyItemViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.key_list_item, parent, false)); + + case VIEW_ITEM_TYPE_DUMMY: + return new KeyDummyViewHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.key_list_dummy, parent, false)); + + default: + return null; + } + + } + + @Override + protected void onBindSectionViewHolder(KeyHeaderViewHolder holder, Character section) { + switch (holder.getItemViewTypeWithoutSections()) { + case VIEW_SECTION_TYPE_PUBLIC: { + String title = section.equals('?') ? + getContext().getString(R.string.user_id_no_name) : + String.valueOf(section); + + holder.bind(title); + break; + } + + case VIEW_SECTION_TYPE_PRIVATE: { + int count = getCount(); + String title = getContext().getResources() + .getQuantityString(R.plurals.n_keys, count, count); + holder.bind(title); + break; + } + + } + } + + @Override + protected void onBindItemViewHolder(ViewHolder holder, Cursor cursor) { + if (holder.getItemViewTypeWithoutSections() == VIEW_ITEM_TYPE_KEY) { Highlighter highlighter = new Highlighter(getContext(), mQuery); - holder.bindKey(new KeyItem(cursor), highlighter); + ((KeyItemViewHolder) holder).bindKey(new KeyItem(cursor), highlighter); } } - static class KeyItemViewHolder extends RecyclerView.ViewHolder { - private View mLayoutDummy; + private static class KeyDummyViewHolder extends SectionCursorAdapter.ViewHolder + implements View.OnClickListener{ + + public KeyDummyViewHolder(View itemView) { + super(itemView); + + itemView.setClickable(true); + itemView.setOnClickListener(this); + } + + @Override + public void onClick(View view) { + + } + } + + private static class KeyItemViewHolder extends SectionCursorAdapter.ViewHolder + implements View.OnClickListener, View.OnLongClickListener { + private View mLayoutData; private Long mMasterKeyId; private TextView mMainUserId; @@ -130,22 +231,23 @@ public class KeySectionedListAdapter extends SectionCursorAdapter 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 abstract class SectionCursorAdapter extends CursorAdapter { public static final String TAG = "SectionCursorAdapter"; - private static final int VIEW_TYPE_ITEM = 0x0; - private static final int VIEW_TYPE_SECTION = 0x1; + private static final short VIEW_TYPE_ITEM = 0x1; + private static final short VIEW_TYPE_SECTION = 0x2; 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() { @@ -55,8 +46,6 @@ public abstract class SectionCursorAdapter 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. @@ -242,10 +173,7 @@ public abstract class SectionCursorAdapter 0; return sectionIndex == 0 && hasSections && listPosition < mSectionMap.keyAt(0); @@ -292,7 +199,21 @@ public abstract class SectionCursorAdapter> 16); case VIEW_TYPE_ITEM: - return onCreateItemViewHolder(parent, viewType); + return onCreateItemViewHolder(parent, viewType >> 16); default: return null; } } - protected abstract SH onCreateSectionViewHolder(ViewGroup parent); + protected abstract SH onCreateSectionViewHolder(ViewGroup parent, int viewType); protected abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType); - protected abstract void onBindSectionViewHolder(SH holder, int sectionIndex, T section); + protected abstract void onBindSectionViewHolder(SH holder, T section); protected abstract void onBindItemViewHolder(VH holder, Cursor cursor); public interface Comparator { boolean equal(T obj1, T obj2); } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(View itemView) { + super(itemView); + } + + /** + * Returns the view type assigned in + * {@link SectionCursorAdapter#getSectionHeaderViewType(int)} or + * {@link SectionCursorAdapter#getSectionItemViewType(int)} + * + * Note that a call to {@link #getItemViewType()} will return a value that contains + * internal stuff necessary to distinguish sections from items. + * @return The view type you set. + */ + public short getItemViewTypeWithoutSections(){ + return (short) (getItemViewType() >> 16); + } + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerFragment.java new file mode 100644 index 000000000..b6deb0362 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerFragment.java @@ -0,0 +1,397 @@ +/* + * Implementation of taken from the sourcecode of + * android.support.v4.app.ListFragment from the + * Android Open Source Project and changed to use + * RecyclerView instead of ListView. + */ + +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.sufficientlysecure.keychain.ui.util.recyclerview; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.Fragment; +import android.support.v7.widget.RecyclerView; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.util.Log; + +public class RecyclerFragment extends Fragment { + + private static final int INTERNAL_LIST_VIEW_ID = android.R.id.list; + private static final int INTERNAL_EMPTY_VIEW_ID = android.R.id.empty; + private static final int INTERNAL_LIST_CONTAINER_ID = android.R.id.widget_frame; + private static final int INTERNAL_PROGRESS_CONTAINER_ID = android.R.id.progress; + + private final Handler handler = new Handler(); + private final Runnable requestFocus = new Runnable() { + @Override + public void run() { + listView.focusableViewAvailable(listView); + } + }; + + private boolean observerRegistered = false; + private final RecyclerView.AdapterDataObserver dataObserver = new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + super.onChanged(); + checkDataSet(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + super.onItemRangeInserted(positionStart, itemCount); + checkDataSet(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + super.onItemRangeRemoved(positionStart, itemCount); + checkDataSet(); + } + }; + + private final RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() { + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + RecyclerFragment.this.onScrolled(dx, dy); + } + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + RecyclerFragment.this.onScrollStateChanged(newState); + } + }; + + private A adapter; + private RecyclerView.LayoutManager layoutManager; + private RecyclerView listView; + private View emptyView; + private View progressContainer; + private View listContainer; + private boolean listShown; + + public RecyclerFragment() { + super(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { + + final Context context = parent.getContext(); + FrameLayout root = new FrameLayout(context); + + LinearLayout progressContainer = new LinearLayout(context); + progressContainer.setId(INTERNAL_PROGRESS_CONTAINER_ID); + progressContainer.setOrientation(LinearLayout.VERTICAL); + progressContainer.setGravity(Gravity.CENTER); + progressContainer.setVisibility(View.GONE); + + ProgressBar progressBar = new ProgressBar(context, null, + android.R.attr.progressBarStyleLarge); + + progressContainer.addView(progressBar, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + root.addView(progressContainer, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + + FrameLayout listContainer = new FrameLayout(context); + listContainer.setId(INTERNAL_LIST_CONTAINER_ID); + + TextView textView = new TextView(context); + textView.setId(INTERNAL_EMPTY_VIEW_ID); + textView.setGravity(Gravity.CENTER); + + listContainer.addView(textView, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + RecyclerView listView = new RecyclerView(context); + listView.setId(INTERNAL_LIST_VIEW_ID); + + listContainer.addView(listView, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + + root.addView(listContainer, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + + root.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + return root; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ensureList(); + } + + @Override + public void onDestroyView() { + handler.removeCallbacks(requestFocus); + listView.setLayoutManager(null); + listView.removeOnScrollListener(scrollListener); + + listView = null; + listShown = false; + listContainer = null; + layoutManager = null; + emptyView = null; + progressContainer = null; + + super.onDestroyView(); + } + + @Override + public void onDestroy() { + setAdapter(null); + super.onDestroy(); + } + + public Handler getHandler() { + return handler; + } + + public void onScrollStateChanged(int state) { + // empty body + } + + public void onScrolled(int dx, int dy) { + // empty body + } + + public void setAdapter(A adapter) { + unregisterObserver(); + + boolean hadAdapter = this.adapter != null; + this.adapter = adapter; + + registerObserver(); + + if(listView != null) { + listView.setAdapter(adapter); + + if(!listShown && !hadAdapter) { + if(getView() != null) + setListShown(true, getView().getWindowToken() != null); + } + } + } + + public void setLayoutManager(RecyclerView.LayoutManager manager) { + if(!manager.isAttachedToWindow()) { + layoutManager = manager; + + if (listView != null) { + listView.setLayoutManager(manager); + } + } + } + + public int getItemCount() { + if(adapter != null) + return adapter.getItemCount(); + else + return 0; + } + + public long getItemId(int position) { + if(adapter != null) + return adapter.getItemId(position); + else + return View.NO_ID; + } + + public RecyclerView getRecyclerView() { + ensureList(); + return listView; + } + + public RecyclerView.LayoutManager getLayoutManager() { + ensureList(); + return layoutManager; + } + + private void registerObserver() { + if(!observerRegistered && adapter != null) { + adapter.registerAdapterDataObserver(dataObserver); + observerRegistered = true; + } + } + + private void unregisterObserver() { + if(observerRegistered && adapter != null) { + adapter.unregisterAdapterDataObserver(dataObserver); + observerRegistered = false; + } + } + + private void checkDataSet() { + boolean empty = treatAsEmpty(getItemCount()); + + Log.d("RecyclerFragment", "Dataset change detected! Count: " + + getItemCount() + ", Empty: " + empty); + + if(emptyView != null) { + emptyView.setVisibility(empty ? View.VISIBLE : View.GONE); + } + } + + /** + * Set whether the data set of the recycler view should be treated as empty. + * This is useful e.g. if you have an empty padding row and therefore the item + * count is always greater than 0. + * + * @param itemCount the number of items in the data set. + * @return Whether to treat this as an empty set of data + */ + protected boolean treatAsEmpty(int itemCount) { + return itemCount < 1; + } + + /** + * Set whether the recycler view should have a fixed size or not + */ + protected boolean isFixedSize() { + return true; + } + + public void hideList(boolean animated) { + setListShown(false, animated); + } + + public void showList(boolean animated) { + setListShown(true, animated); + } + + private void setListShown(boolean shown, boolean animated) { + ensureList(); + + if(progressContainer == null) + throw new IllegalStateException("Can't be used with a custom content view"); + + if(listShown == shown) + return; + + listShown = shown; + if(shown) { + if (animated) { + progressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + listContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + } else { + progressContainer.clearAnimation(); + listContainer.clearAnimation(); + } + progressContainer.setVisibility(View.GONE); + listContainer.setVisibility(View.VISIBLE); + } else { + if (animated) { + progressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + listContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + } else { + progressContainer.clearAnimation(); + listContainer.clearAnimation(); + } + progressContainer.setVisibility(View.VISIBLE); + listContainer.setVisibility(View.GONE); + } + } + + public A getAdapter() { + return adapter; + } + + @SuppressWarnings("unchecked") + private void ensureList() { + if (listView != null) + return; + + View root = getView(); + if (root == null) + throw new IllegalStateException("Content view not yet created"); + + if (root instanceof RecyclerView) { + listView = (RecyclerView) root; + } else { + emptyView = root.findViewById(INTERNAL_EMPTY_VIEW_ID); + if(emptyView != null) { + emptyView.setVisibility(View.GONE); + } + + progressContainer = root.findViewById(INTERNAL_PROGRESS_CONTAINER_ID); + listContainer = root.findViewById(INTERNAL_LIST_CONTAINER_ID); + + View rawListView = root.findViewById(INTERNAL_LIST_VIEW_ID); + if (!(rawListView instanceof RecyclerView)) { + if (rawListView == null) { + throw new RuntimeException( + "Your content must have a RecyclerView whose id attribute is " + + "'android.R.id.list'"); + } + throw new RuntimeException( + "Content has view with id attribute 'android.R.id.list' " + + "that is not a ListView class"); + } + + listView = (RecyclerView) rawListView; + } + + if(layoutManager != null) { + RecyclerView.LayoutManager manager = layoutManager; + this.layoutManager = null; + setLayoutManager(manager); + } + + listShown = true; + listView.setHasFixedSize(isFixedSize()); + listView.addOnScrollListener(scrollListener); + + if (this.adapter != null) { + A adapter = this.adapter; + this.adapter = null; + setAdapter(adapter); + } else { + // We are starting without an adapter, so assume we won't + // have our data right away and start with the progress indicator. + if (progressContainer != null) { + setListShown(false, false); + } + } + + handler.post(requestFocus); + } +} diff --git a/OpenKeychain/src/main/res/layout/key_list_dummy.xml b/OpenKeychain/src/main/res/layout/key_list_dummy.xml new file mode 100644 index 000000000..afdd88f0c --- /dev/null +++ b/OpenKeychain/src/main/res/layout/key_list_dummy.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/key_list_fragment.xml b/OpenKeychain/src/main/res/layout/key_list_fragment.xml index ce40dac8c..8f79d9c4e 100644 --- a/OpenKeychain/src/main/res/layout/key_list_fragment.xml +++ b/OpenKeychain/src/main/res/layout/key_list_fragment.xml @@ -1,115 +1,140 @@ - + xmlns:android="http://schemas.android.com/apk/res/android" - - + + + android:layout_height="match_parent" + android:gravity="center"> - + + + + + + + + android:layout_height="match_parent"> - + - + android:orientation="vertical" + android:animateLayoutChanges="true" > - - - - -