First try implementing sticky section headers
This commit is contained in:
@@ -33,6 +33,7 @@ dependencies {
|
|||||||
compile 'org.apache.james:apache-mime4j-dom:0.7.2'
|
compile 'org.apache.james:apache-mime4j-dom:0.7.2'
|
||||||
compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0'
|
compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0'
|
||||||
compile 'com.cocosw:bottomsheet:1.3.0@aar'
|
compile 'com.cocosw:bottomsheet:1.3.0@aar'
|
||||||
|
compile 'com.tonicartos:superslim:0.4.13'
|
||||||
|
|
||||||
// Material Drawer
|
// Material Drawer
|
||||||
compile 'com.mikepenz:materialdrawer:5.2.2@aar'
|
compile 'com.mikepenz:materialdrawer:5.2.2@aar'
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import android.support.v4.app.LoaderManager;
|
|||||||
import android.support.v4.content.CursorLoader;
|
import android.support.v4.content.CursorLoader;
|
||||||
import android.support.v4.content.Loader;
|
import android.support.v4.content.Loader;
|
||||||
import android.support.v4.view.MenuItemCompat;
|
import android.support.v4.view.MenuItemCompat;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.SearchView;
|
import android.support.v7.widget.SearchView;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.ActionMode;
|
import android.view.ActionMode;
|
||||||
@@ -54,6 +55,8 @@ import android.widget.ViewAnimator;
|
|||||||
|
|
||||||
import com.getbase.floatingactionbutton.FloatingActionButton;
|
import com.getbase.floatingactionbutton.FloatingActionButton;
|
||||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
||||||
|
import com.tonicartos.superslim.LayoutManager;
|
||||||
|
|
||||||
import org.sufficientlysecure.keychain.Constants;
|
import org.sufficientlysecure.keychain.Constants;
|
||||||
import org.sufficientlysecure.keychain.R;
|
import org.sufficientlysecure.keychain.R;
|
||||||
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
|
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.KeyFormattingUtils;
|
||||||
import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint;
|
import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint;
|
||||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
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.FabContainer;
|
||||||
import org.sufficientlysecure.keychain.util.Log;
|
import org.sufficientlysecure.keychain.util.Log;
|
||||||
import org.sufficientlysecure.keychain.util.Preferences;
|
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_DELETE = 2;
|
||||||
private static final int REQUEST_VIEW_KEY = 3;
|
private static final int REQUEST_VIEW_KEY = 3;
|
||||||
|
|
||||||
private KeyListAdapter mAdapter;
|
//private KeyListAdapter mAdapter;
|
||||||
private StickyListHeadersListView mStickyList;
|
private KeySectionedListAdapter mAdapter;
|
||||||
|
private RecyclerView mStickyList;
|
||||||
|
|
||||||
// saves the mode object for multiselect, needed for reset at some point
|
// saves the mode object for multiselect, needed for reset at some point
|
||||||
private ActionMode mActionMode = null;
|
private ActionMode mActionMode = null;
|
||||||
@@ -125,8 +130,8 @@ public class KeyListFragment extends LoaderFragment
|
|||||||
View root = super.onCreateView(inflater, superContainer, savedInstanceState);
|
View root = super.onCreateView(inflater, superContainer, savedInstanceState);
|
||||||
View view = inflater.inflate(R.layout.key_list_fragment, getContainer());
|
View view = inflater.inflate(R.layout.key_list_fragment, getContainer());
|
||||||
|
|
||||||
mStickyList = (StickyListHeadersListView) view.findViewById(R.id.key_list_list);
|
mStickyList = (RecyclerView) view.findViewById(R.id.key_list_list);
|
||||||
mStickyList.setOnItemClickListener(this);
|
//mStickyList.setOnItemClickListener(this);
|
||||||
|
|
||||||
mFab = (FloatingActionsMenu) view.findViewById(R.id.fab_main);
|
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
|
// show app name instead of "keys" from nav drawer
|
||||||
final FragmentActivity activity = getActivity();
|
final FragmentActivity activity = getActivity();
|
||||||
|
|
||||||
activity.setTitle(R.string.app_name);
|
activity.setTitle(R.string.app_name);
|
||||||
|
|
||||||
mStickyList.setOnItemClickListener(this);
|
//mStickyList.setOnItemClickListener(this);
|
||||||
mStickyList.setAreHeadersSticky(true);
|
//mStickyList.setAreHeadersSticky(true);
|
||||||
mStickyList.setDrawingListUnderStickyHeader(false);
|
//mStickyList.setDrawingListUnderStickyHeader(false);
|
||||||
mStickyList.setFastScrollEnabled(true);
|
//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.
|
// in last few rows.
|
||||||
@@ -192,14 +196,16 @@ public class KeyListFragment extends LoaderFragment
|
|||||||
);
|
);
|
||||||
|
|
||||||
footer.setLayoutParams(params);
|
footer.setLayoutParams(params);
|
||||||
mStickyList.addFooterView(footer, null, false);
|
//mStickyList.addFooterView(footer, null, false);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Multi-selection
|
* 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() {
|
mStickyList.getWrappedList().setMultiChoiceModeListener(new MultiChoiceModeListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -257,6 +263,7 @@ public class KeyListFragment extends LoaderFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
// We have a menu item to show in action bar.
|
// We have a menu item to show in action bar.
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
@@ -265,7 +272,7 @@ public class KeyListFragment extends LoaderFragment
|
|||||||
setContentShown(false);
|
setContentShown(false);
|
||||||
|
|
||||||
// this view is made visible if no data is available
|
// 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
|
// click on search button (in empty view) starts query for search string
|
||||||
vSearchContainer = (ViewAnimator) activity.findViewById(R.id.search_container);
|
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.
|
// 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.setAdapter(mAdapter);
|
||||||
|
mStickyList.setLayoutManager(new LayoutManager(getActivity()));
|
||||||
|
|
||||||
// Prepare the loader. Either re-connect with an existing one,
|
// Prepare the loader. Either re-connect with an existing one,
|
||||||
// or start a new one.
|
// or start a new one.
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
package org.sufficientlysecure.keychain.ui.util.adapter;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.ContentObserver;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.DataSetObserver;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.sufficientlysecure.keychain.util.Log;
|
||||||
|
|
||||||
|
public abstract class CursorAdapter extends RecyclerView.Adapter {
|
||||||
|
|
||||||
|
public static final String TAG = "CursorAdapter";
|
||||||
|
|
||||||
|
private Cursor mCursor;
|
||||||
|
private Context mContext;
|
||||||
|
private boolean mDataValid;
|
||||||
|
|
||||||
|
private ChangeObserver mChangeObserver;
|
||||||
|
private DataSetObserver mDataSetObserver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set the adapter will register a content observer on the cursor and will call
|
||||||
|
* {@link #onContentChanged()} when a notification comes in. Be careful when
|
||||||
|
* using this flag: you will need to unset the current Cursor from the adapter
|
||||||
|
* to avoid leaks due to its registered observers. This flag is not needed
|
||||||
|
* when using a CursorAdapter with a
|
||||||
|
* {@link android.content.CursorLoader}.
|
||||||
|
*/
|
||||||
|
public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor that allows control over auto-requery. It is recommended
|
||||||
|
* you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}.
|
||||||
|
* When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
|
||||||
|
* will always be set.
|
||||||
|
*
|
||||||
|
* @param c The cursor from which to get the data.
|
||||||
|
* @param context The context
|
||||||
|
*/
|
||||||
|
public CursorAdapter(Context context, Cursor c) {
|
||||||
|
init(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recommended constructor.
|
||||||
|
*
|
||||||
|
* @param c The cursor from which to get the data.
|
||||||
|
* @param context The context
|
||||||
|
* @param flags Flags used to determine the behavior of the adapter
|
||||||
|
* @see #FLAG_REGISTER_CONTENT_OBSERVER
|
||||||
|
*/
|
||||||
|
public CursorAdapter(Context context, Cursor c, int flags) {
|
||||||
|
init(context, c, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(Context context, Cursor c, int flags) {
|
||||||
|
boolean cursorPresent = c != null;
|
||||||
|
mCursor = c;
|
||||||
|
mDataValid = cursorPresent;
|
||||||
|
mContext = context;
|
||||||
|
if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||||
|
mChangeObserver = new ChangeObserver();
|
||||||
|
mDataSetObserver = new MyDataSetObserver();
|
||||||
|
} else {
|
||||||
|
mChangeObserver = null;
|
||||||
|
mDataSetObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorPresent) {
|
||||||
|
if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
|
||||||
|
if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasStableIds(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cursor.
|
||||||
|
* @return the cursor.
|
||||||
|
*/
|
||||||
|
public Cursor getCursor() {
|
||||||
|
return mCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Context getContext() {
|
||||||
|
return mContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see android.support.v7.widget.RecyclerView.Adapter#getItemCount()
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
if (mDataValid && mCursor != null) {
|
||||||
|
return mCursor.getCount();
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasValidData() {
|
||||||
|
mDataValid = hasOpenCursor();
|
||||||
|
return mDataValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasOpenCursor() {
|
||||||
|
Cursor cursor = getCursor();
|
||||||
|
if (cursor != null && cursor.isClosed()) {
|
||||||
|
swapCursor(null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see android.support.v7.widget.RecyclerView.Adapter#getItemId(int)
|
||||||
|
*
|
||||||
|
* @param position Adapter position to query
|
||||||
|
* @return the id of the item
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
if (mDataValid && mCursor != null) {
|
||||||
|
if (moveCursor(position)) {
|
||||||
|
return getIdFromCursor(mCursor);
|
||||||
|
} else {
|
||||||
|
return RecyclerView.NO_ID;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return RecyclerView.NO_ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the id of the item represented by the row the cursor
|
||||||
|
* is currently moved to.
|
||||||
|
* @param cursor The cursor moved to the correct position.
|
||||||
|
* @return The id of the dataset
|
||||||
|
*/
|
||||||
|
public long getIdFromCursor(Cursor cursor) {
|
||||||
|
if(cursor != null) {
|
||||||
|
return cursor.getPosition();
|
||||||
|
} else {
|
||||||
|
return RecyclerView.NO_ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void moveCursorOrThrow(int position)
|
||||||
|
throws IndexOutOfBoundsException, IllegalStateException {
|
||||||
|
|
||||||
|
if(position >= getItemCount() || position < -1) {
|
||||||
|
throw new IndexOutOfBoundsException("Position: " + position
|
||||||
|
+ " is invalid for this data set!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!mDataValid) {
|
||||||
|
throw new IllegalStateException("Attempt to move cursor over invalid data set!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!mCursor.moveToPosition(position)) {
|
||||||
|
throw new IllegalStateException("Couldn't move cursor from position: "
|
||||||
|
+ mCursor.getPosition() + " to position: " + position + "!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean moveCursor(int position) {
|
||||||
|
if(position >= getItemCount() || position < -1) {
|
||||||
|
Log.w(TAG, "Position: %d is invalid for this data set!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!mDataValid) {
|
||||||
|
Log.d(TAG, "Attempt to move cursor over invalid data set!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return mCursor.moveToPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the underlying cursor to a new cursor. If there is an existing cursor it will be
|
||||||
|
* closed.
|
||||||
|
*
|
||||||
|
* @param cursor The new cursor to be used
|
||||||
|
*/
|
||||||
|
public void changeCursor(Cursor cursor) {
|
||||||
|
Cursor old = swapCursor(cursor);
|
||||||
|
if (old != null) {
|
||||||
|
old.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swap in a new Cursor, returning the old Cursor. Unlike
|
||||||
|
* {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
|
||||||
|
* closed.
|
||||||
|
*
|
||||||
|
* @param newCursor The new cursor to be used.
|
||||||
|
* @return Returns the previously set Cursor, or null if there wasa not one.
|
||||||
|
* If the given new Cursor is the same instance is the previously set
|
||||||
|
* Cursor, null is also returned.
|
||||||
|
*/
|
||||||
|
public Cursor swapCursor(Cursor newCursor) {
|
||||||
|
if (newCursor == mCursor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cursor oldCursor = mCursor;
|
||||||
|
if (oldCursor != null) {
|
||||||
|
if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
|
||||||
|
if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
mCursor = newCursor;
|
||||||
|
if (newCursor != null) {
|
||||||
|
if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
|
||||||
|
if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
|
||||||
|
mDataValid = true;
|
||||||
|
// notify the observers about the new cursor
|
||||||
|
onContentChanged();
|
||||||
|
} else {
|
||||||
|
mDataValid = false;
|
||||||
|
// notify the observers about the lack of a data set
|
||||||
|
onContentChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Converts the cursor into a CharSequence. Subclasses should override this
|
||||||
|
* method to convert their results. The default implementation returns an
|
||||||
|
* empty String for null values or the default String representation of
|
||||||
|
* the value.</p>
|
||||||
|
*
|
||||||
|
* @param cursor the cursor to convert to a CharSequence
|
||||||
|
* @return a CharSequence representing the value
|
||||||
|
*/
|
||||||
|
public CharSequence convertToString(Cursor cursor) {
|
||||||
|
return cursor == null ? "" : cursor.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the {@link ContentObserver} on the cursor receives a change notification.
|
||||||
|
* The default implementation provides the auto-requery logic, but may be overridden by
|
||||||
|
* sub classes.
|
||||||
|
*
|
||||||
|
* @see ContentObserver#onChange(boolean)
|
||||||
|
*/
|
||||||
|
protected void onContentChanged() {
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ChangeObserver extends ContentObserver {
|
||||||
|
public ChangeObserver() {
|
||||||
|
super(new Handler());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deliverSelfNotifications() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onChange(boolean selfChange) {
|
||||||
|
onContentChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MyDataSetObserver extends DataSetObserver {
|
||||||
|
@Override
|
||||||
|
public void onChanged() {
|
||||||
|
mDataValid = true;
|
||||||
|
onContentChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInvalidated() {
|
||||||
|
mDataValid = false;
|
||||||
|
onContentChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package org.sufficientlysecure.keychain.ui.util.adapter;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.text.format.DateUtils;
|
||||||
|
import android.util.SparseBooleanArray;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||||
|
import org.sufficientlysecure.keychain.R;
|
||||||
|
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||||
|
import org.sufficientlysecure.keychain.ui.util.Highlighter;
|
||||||
|
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||||
|
|
||||||
|
public class KeySectionedListAdapter extends SectionCursorAdapter<String, KeySectionedListAdapter.KeyItemViewHolder, KeySectionedListAdapter.KeyHeaderViewHolder> implements KeyAdapter {
|
||||||
|
private String mQuery;
|
||||||
|
private SparseBooleanArray mSelectionMap;
|
||||||
|
|
||||||
|
|
||||||
|
public KeySectionedListAdapter(Context context, Cursor cursor) {
|
||||||
|
super(context, cursor, 0);
|
||||||
|
|
||||||
|
mQuery = "";
|
||||||
|
mSelectionMap = new SparseBooleanArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSearchQuery(String query) {
|
||||||
|
mQuery = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled(Cursor cursor) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyItem getItem(int position) {
|
||||||
|
Cursor cursor = getCursor();
|
||||||
|
|
||||||
|
if(cursor != null) {
|
||||||
|
if(cursor.getPosition() != position) {
|
||||||
|
moveCursor(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KeyItem(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getMasterKeyId(int position) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSecretAvailable(int position) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getSectionFromCursor(Cursor cursor) throws IllegalStateException {
|
||||||
|
if (cursor.getInt(INDEX_HAS_ANY_SECRET) != 0) {
|
||||||
|
return getContext().getString(R.string.my_keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
String userId = cursor.getString(INDEX_USER_ID);
|
||||||
|
String headerText = getContext().getString(R.string.user_id_no_name);
|
||||||
|
|
||||||
|
if (userId != null && userId.length() > 0) {
|
||||||
|
headerText = "" + userId.charAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerText;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected KeyHeaderViewHolder onCreateSectionViewHolder(ViewGroup parent) {
|
||||||
|
return new KeyHeaderViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.key_list_header, parent, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected KeyItemViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
return new KeyItemViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.key_list_item, parent, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBindSectionViewHolder(KeyHeaderViewHolder holder, int sectionIndex, String section) {
|
||||||
|
System.out.println("SIX: " + sectionIndex);
|
||||||
|
if(sectionIndex == 0) {
|
||||||
|
holder.bind(section, getCursor().getCount());
|
||||||
|
} else {
|
||||||
|
holder.bind(section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBindItemViewHolder(KeyItemViewHolder holder, Cursor cursor) {
|
||||||
|
boolean isSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0;
|
||||||
|
long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
|
||||||
|
if (isSecret && masterKeyId == 0L) {
|
||||||
|
holder.bindDummy();
|
||||||
|
} else {
|
||||||
|
Highlighter highlighter = new Highlighter(getContext(), mQuery);
|
||||||
|
holder.bindKey(new KeyItem(cursor), highlighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeyItemViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
private View mLayoutDummy;
|
||||||
|
private View mLayoutData;
|
||||||
|
private Long mMasterKeyId;
|
||||||
|
private TextView mMainUserId;
|
||||||
|
private TextView mMainUserIdRest;
|
||||||
|
private TextView mCreationDate;
|
||||||
|
private ImageView mStatus;
|
||||||
|
private View mSlinger;
|
||||||
|
private ImageButton mSlingerButton;
|
||||||
|
|
||||||
|
public KeyItemViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
|
||||||
|
mLayoutData = itemView.findViewById(R.id.key_list_item_data);
|
||||||
|
mLayoutDummy = itemView.findViewById(R.id.key_list_item_dummy);
|
||||||
|
mMainUserId = (TextView) itemView.findViewById(R.id.key_list_item_name);
|
||||||
|
mMainUserIdRest = (TextView) itemView.findViewById(R.id.key_list_item_email);
|
||||||
|
mStatus = (ImageView) itemView.findViewById(R.id.key_list_item_status_icon);
|
||||||
|
mSlinger = itemView.findViewById(R.id.key_list_item_slinger_view);
|
||||||
|
mSlingerButton = (ImageButton) itemView.findViewById(R.id.key_list_item_slinger_button);
|
||||||
|
mCreationDate = (TextView) itemView.findViewById(R.id.key_list_item_creation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bindKey(KeyItem keyItem, Highlighter highlighter) {
|
||||||
|
Context ctx = itemView.getContext();
|
||||||
|
|
||||||
|
mLayoutData.setVisibility(View.VISIBLE);
|
||||||
|
mLayoutDummy.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
{ // set name and stuff, common to both key types
|
||||||
|
OpenPgpUtils.UserId userIdSplit = keyItem.mUserId;
|
||||||
|
if (userIdSplit.name != null) {
|
||||||
|
mMainUserId.setText(highlighter.highlight(userIdSplit.name));
|
||||||
|
} else {
|
||||||
|
mMainUserId.setText(R.string.user_id_no_name);
|
||||||
|
}
|
||||||
|
if (userIdSplit.email != null) {
|
||||||
|
mMainUserIdRest.setText(highlighter.highlight(userIdSplit.email));
|
||||||
|
mMainUserIdRest.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
mMainUserIdRest.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort of a hack: if this item isn't enabled, we make it clickable
|
||||||
|
// to intercept its click events. either way, no listener!
|
||||||
|
itemView.setClickable(false);
|
||||||
|
|
||||||
|
{ // set edit button and status, specific by key type
|
||||||
|
|
||||||
|
mMasterKeyId = keyItem.mKeyId;
|
||||||
|
|
||||||
|
int textColor;
|
||||||
|
|
||||||
|
// Note: order is important!
|
||||||
|
if (keyItem.mIsRevoked) {
|
||||||
|
KeyFormattingUtils
|
||||||
|
.setStatusImage(ctx, mStatus, null, KeyFormattingUtils.State.REVOKED, R.color.key_flag_gray);
|
||||||
|
mStatus.setVisibility(View.VISIBLE);
|
||||||
|
mSlinger.setVisibility(View.GONE);
|
||||||
|
textColor = ctx.getResources().getColor(R.color.key_flag_gray);
|
||||||
|
} else if (keyItem.mIsExpired) {
|
||||||
|
KeyFormattingUtils.setStatusImage(ctx, mStatus, null, KeyFormattingUtils.State.EXPIRED, R.color.key_flag_gray);
|
||||||
|
mStatus.setVisibility(View.VISIBLE);
|
||||||
|
mSlinger.setVisibility(View.GONE);
|
||||||
|
textColor = ctx.getResources().getColor(R.color.key_flag_gray);
|
||||||
|
} else if (keyItem.mIsSecret) {
|
||||||
|
mStatus.setVisibility(View.GONE);
|
||||||
|
if (mSlingerButton.hasOnClickListeners()) {
|
||||||
|
mSlingerButton.setColorFilter(
|
||||||
|
FormattingUtils.getColorFromAttr(ctx, R.attr.colorTertiaryText),
|
||||||
|
PorterDuff.Mode.SRC_IN);
|
||||||
|
mSlinger.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
mSlinger.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
textColor = FormattingUtils.getColorFromAttr(ctx, R.attr.colorText);
|
||||||
|
} else {
|
||||||
|
// this is a public key - show if it's verified
|
||||||
|
if (keyItem.mIsVerified) {
|
||||||
|
KeyFormattingUtils.setStatusImage(ctx, mStatus, KeyFormattingUtils.State.VERIFIED);
|
||||||
|
mStatus.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
KeyFormattingUtils.setStatusImage(ctx, mStatus, KeyFormattingUtils.State.UNVERIFIED);
|
||||||
|
mStatus.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
mSlinger.setVisibility(View.GONE);
|
||||||
|
textColor = FormattingUtils.getColorFromAttr(ctx, R.attr.colorText);
|
||||||
|
}
|
||||||
|
|
||||||
|
mMainUserId.setTextColor(textColor);
|
||||||
|
mMainUserIdRest.setTextColor(textColor);
|
||||||
|
|
||||||
|
if (keyItem.mHasDuplicate) {
|
||||||
|
String dateTime = DateUtils.formatDateTime(ctx,
|
||||||
|
keyItem.mCreation.getTime(),
|
||||||
|
DateUtils.FORMAT_SHOW_DATE
|
||||||
|
| DateUtils.FORMAT_SHOW_TIME
|
||||||
|
| DateUtils.FORMAT_SHOW_YEAR
|
||||||
|
| DateUtils.FORMAT_ABBREV_MONTH);
|
||||||
|
mCreationDate.setText(ctx.getString(R.string.label_key_created,
|
||||||
|
dateTime));
|
||||||
|
mCreationDate.setTextColor(textColor);
|
||||||
|
mCreationDate.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
mCreationDate.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bindDummy() {
|
||||||
|
// just reset everything to display the dummy layout
|
||||||
|
mLayoutDummy.setVisibility(View.VISIBLE);
|
||||||
|
mLayoutData.setVisibility(View.GONE);
|
||||||
|
mSlinger.setVisibility(View.GONE);
|
||||||
|
mStatus.setVisibility(View.GONE);
|
||||||
|
itemView.setClickable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KeyHeaderViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
private TextView mHeaderText;
|
||||||
|
private TextView mHeaderCount;
|
||||||
|
|
||||||
|
public KeyHeaderViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
|
||||||
|
mHeaderText = (TextView) itemView.findViewById(R.id.stickylist_header_text);
|
||||||
|
mHeaderCount = (TextView) itemView.findViewById(R.id.contacts_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bind(String title, int count) {
|
||||||
|
mHeaderText.setText(title);
|
||||||
|
|
||||||
|
String contactsTotal = itemView.getResources()
|
||||||
|
.getQuantityString(R.plurals.n_keys, count, count);
|
||||||
|
|
||||||
|
mHeaderCount.setText(contactsTotal);
|
||||||
|
mHeaderCount.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bind(String title) {
|
||||||
|
mHeaderText.setText(title);
|
||||||
|
mHeaderCount.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
package org.sufficientlysecure.keychain.ui.util.adapter;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.support.v4.util.SparseArrayCompat;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.SectionIndexer;
|
||||||
|
|
||||||
|
import com.tonicartos.superslim.GridSLM;
|
||||||
|
import com.tonicartos.superslim.LayoutManager;
|
||||||
|
import com.tonicartos.superslim.LinearSLM;
|
||||||
|
|
||||||
|
import org.sufficientlysecure.keychain.util.Log;
|
||||||
|
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
/**
|
||||||
|
* @param <T> section type.
|
||||||
|
* @param <VH> the view holder extending {@code BaseViewHolder<Cursor>} that is bound to the cursor data.
|
||||||
|
* @param <SH> the view holder extending {@code BaseViewHolder<<T>>} that is bound to the section data.
|
||||||
|
*/
|
||||||
|
public abstract class SectionCursorAdapter<T, VH extends RecyclerView.ViewHolder, SH extends RecyclerView.ViewHolder>
|
||||||
|
extends CursorAdapter implements SectionIndexer {
|
||||||
|
|
||||||
|
public static final String TAG = "SectionCursorAdapter";
|
||||||
|
|
||||||
|
private static final int VIEW_TYPE_ITEM = 0x0;
|
||||||
|
private static final int VIEW_TYPE_SECTION = 0x1;
|
||||||
|
|
||||||
|
private SparseArrayCompat<T> mSectionMap = new SparseArrayCompat<>();
|
||||||
|
private ArrayList<Integer> mSectionIndexList = new ArrayList<>();
|
||||||
|
private Comparator<T> mSectionComparator;
|
||||||
|
private Object[] mFastScrollItems;
|
||||||
|
|
||||||
|
public SectionCursorAdapter(Context context, Cursor cursor, int flags) {
|
||||||
|
this(context, cursor, flags, new Comparator<T>() {
|
||||||
|
@Override
|
||||||
|
public boolean equal(T obj1, T obj2) {
|
||||||
|
return (obj1 == null) ?
|
||||||
|
obj2 == null : obj1.equals(obj2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public SectionCursorAdapter(Context context, Cursor cursor, int flags, Comparator<T> comparator) {
|
||||||
|
super(context, cursor, flags);
|
||||||
|
setSectionComparator(comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContentChanged() {
|
||||||
|
if (hasValidData()) {
|
||||||
|
buildSections();
|
||||||
|
} else {
|
||||||
|
mSectionMap.clear();
|
||||||
|
mSectionIndexList.clear();
|
||||||
|
mFastScrollItems = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onContentChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign a comparator which will be used to check whether
|
||||||
|
* a section is contained in the list of sections. The default implementation
|
||||||
|
* will check for null pointers and compare sections using the {@link #equals(Object)} method.
|
||||||
|
* @param comparator The comparator to compare section objects.
|
||||||
|
*/
|
||||||
|
public void setSectionComparator(Comparator<T> comparator) {
|
||||||
|
this.mSectionComparator = comparator;
|
||||||
|
buildSections();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the adapter's cursor is not null then this method will call buildSections(Cursor cursor).
|
||||||
|
*/
|
||||||
|
private void buildSections() {
|
||||||
|
if (hasValidData()) {
|
||||||
|
moveCursor(-1);
|
||||||
|
try {
|
||||||
|
mSectionMap.clear();
|
||||||
|
mSectionIndexList.clear();
|
||||||
|
mFastScrollItems = null;
|
||||||
|
|
||||||
|
appendSections(getCursor());
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
Log.e(TAG, "Couldn't build sections. Perhaps you're moving the cursor" +
|
||||||
|
"in #getSectionFromCursor(Cursor)?", e);
|
||||||
|
swapCursor(null);
|
||||||
|
|
||||||
|
mSectionMap.clear();
|
||||||
|
mSectionIndexList.clear();
|
||||||
|
mFastScrollItems = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void appendSections(Cursor cursor) throws IllegalStateException {
|
||||||
|
int cursorPosition = 0;
|
||||||
|
while(hasValidData() && cursor.moveToNext()) {
|
||||||
|
T section = getSectionFromCursor(cursor);
|
||||||
|
if (cursor.getPosition() != cursorPosition)
|
||||||
|
throw new IllegalStateException("Do not move the cursor's position in getSectionFromCursor.");
|
||||||
|
if (!hasSection(section))
|
||||||
|
mSectionMap.append(cursorPosition + mSectionMap.size(), section);
|
||||||
|
cursorPosition++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSection(T section) {
|
||||||
|
for(int i = 0; i < mSectionMap.size(); i++) {
|
||||||
|
T obj = mSectionMap.valueAt(i);
|
||||||
|
if(mSectionComparator.equal(obj, section))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object which is return will determine what section this cursor position will be in.
|
||||||
|
* @return the section from the cursor at its current position.
|
||||||
|
* This object will be passed to newSectionView and bindSectionView.
|
||||||
|
*/
|
||||||
|
protected abstract T getSectionFromCursor(Cursor cursor) throws IllegalStateException;
|
||||||
|
protected String getTitleFromSection(T section) {
|
||||||
|
return section != null ? section.toString() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return super.getItemCount() + mSectionMap.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final long getItemId(int listPosition) {
|
||||||
|
if (isSection(listPosition))
|
||||||
|
return listPosition;
|
||||||
|
else {
|
||||||
|
int cursorPosition = getCursorPositionWithoutSections(listPosition);
|
||||||
|
return super.getItemId(cursorPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param listPosition the position of the current item in the list with mSectionMap included
|
||||||
|
* @return Whether or not the listPosition points to a section.
|
||||||
|
*/
|
||||||
|
public boolean isSection(int listPosition) {
|
||||||
|
return mSectionMap.indexOfKey(listPosition) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will map a position in the list adapter (which includes mSectionMap) to a position in
|
||||||
|
* the cursor (which does not contain mSectionMap).
|
||||||
|
*
|
||||||
|
* @param listPosition the position of the current item in the list with mSectionMap included
|
||||||
|
* @return the correct position to use with the cursor
|
||||||
|
*/
|
||||||
|
public int getCursorPositionWithoutSections(int listPosition) {
|
||||||
|
if (mSectionMap.size() == 0) {
|
||||||
|
return listPosition;
|
||||||
|
} else if (!isSection(listPosition)) {
|
||||||
|
int sectionIndex = getSectionForPosition(listPosition);
|
||||||
|
if (isListPositionBeforeFirstSection(listPosition, sectionIndex)) {
|
||||||
|
return listPosition;
|
||||||
|
} else {
|
||||||
|
return listPosition - (sectionIndex + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the section object for the index within the array of sections.
|
||||||
|
* @param sectionPosition The section index.
|
||||||
|
* @return The specified section object for this position.
|
||||||
|
*/
|
||||||
|
public T getSection(int sectionPosition) {
|
||||||
|
if (mSectionIndexList.contains(sectionPosition)) {
|
||||||
|
return mSectionMap.get(mSectionIndexList.get(sectionPosition));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all indices at which the first item of a section is placed.
|
||||||
|
* @return The first index of each section.
|
||||||
|
*/
|
||||||
|
public List<Integer> getSectionListPositions() {
|
||||||
|
return mSectionIndexList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int getPositionForSection(int sectionIndex) {
|
||||||
|
if (mSectionIndexList.isEmpty()) {
|
||||||
|
for (int i = 0; i < mSectionMap.size(); i++) {
|
||||||
|
mSectionIndexList.add(mSectionMap.keyAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sectionIndex < mSectionIndexList.size() ?
|
||||||
|
mSectionIndexList.get(sectionIndex) : getItemCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the position of a section, returns its index in the array of sections.
|
||||||
|
* @param sectionPosition The first position of the corresponding section in the array of items.
|
||||||
|
* @return The section index in the array of sections.
|
||||||
|
*/
|
||||||
|
public int getSectionIndexForPosition(int sectionPosition) {
|
||||||
|
if (mSectionIndexList.isEmpty()) {
|
||||||
|
for (int i = 0; i < mSectionMap.size(); i++) {
|
||||||
|
mSectionIndexList.add(mSectionMap.keyAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mSectionIndexList.indexOf(sectionPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the list position of an item in the adapter, returns the
|
||||||
|
* adapter position of the first item of the section the given item belongs to.
|
||||||
|
* @param listPosition The absolute list position.
|
||||||
|
* @return The position of the first item of the section.
|
||||||
|
*/
|
||||||
|
public int getFirstSectionPosition(int listPosition) {
|
||||||
|
int start = 0;
|
||||||
|
for(int i = 0; i <= listPosition; i++) {
|
||||||
|
if(isSection(i)) {
|
||||||
|
start = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int getSectionForPosition(int listPosition) {
|
||||||
|
boolean isSection = false;
|
||||||
|
int numPrecedingSections = 0;
|
||||||
|
for (int i = 0; i < mSectionMap.size(); i++) {
|
||||||
|
int sectionPosition = mSectionMap.keyAt(i);
|
||||||
|
|
||||||
|
if (listPosition > sectionPosition) {
|
||||||
|
numPrecedingSections++;
|
||||||
|
} else if (listPosition == sectionPosition) {
|
||||||
|
isSection = true;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSection ? numPrecedingSections : Math.max(numPrecedingSections - 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object[] getSections() {
|
||||||
|
if(mFastScrollItems == null) {
|
||||||
|
mFastScrollItems = getSectionLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mFastScrollItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object[] getSectionLabels() {
|
||||||
|
if(mSectionMap == null)
|
||||||
|
return new Object[0];
|
||||||
|
|
||||||
|
String[] ret = new String[mSectionMap.size()];
|
||||||
|
for(int i = 0; i < ret.length; i++) {
|
||||||
|
ret[i] = getTitleFromSection(mSectionMap.valueAt(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isListPositionBeforeFirstSection(int listPosition, int sectionIndex) {
|
||||||
|
boolean hasSections = mSectionMap != null && mSectionMap.size() > 0;
|
||||||
|
return sectionIndex == 0 && hasSections && listPosition < mSectionMap.keyAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final int getItemViewType(int listPosition) {
|
||||||
|
return isSection(listPosition) ? VIEW_TYPE_SECTION : VIEW_TYPE_ITEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||||
|
LayoutManager.LayoutParams layoutParams = LayoutManager.LayoutParams
|
||||||
|
.from(holder.itemView.getLayoutParams());
|
||||||
|
|
||||||
|
// assign first position of section to each item
|
||||||
|
layoutParams.setFirstPosition(getFirstSectionPosition(position));
|
||||||
|
|
||||||
|
switch (holder.getItemViewType()) {
|
||||||
|
case VIEW_TYPE_ITEM :
|
||||||
|
moveCursorOrThrow(getCursorPositionWithoutSections(position));
|
||||||
|
onBindItemViewHolder((VH) holder, getCursor());
|
||||||
|
|
||||||
|
layoutParams.isHeader = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case VIEW_TYPE_SECTION:
|
||||||
|
T section = mSectionMap.get(position);
|
||||||
|
int sectionIndex = getSectionIndexForPosition(position);
|
||||||
|
onBindSectionViewHolder((SH) holder, sectionIndex, section);
|
||||||
|
|
||||||
|
layoutParams.isHeader = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemView.setLayoutParams(layoutParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
switch (viewType) {
|
||||||
|
case VIEW_TYPE_SECTION:
|
||||||
|
return onCreateSectionViewHolder(parent);
|
||||||
|
|
||||||
|
case VIEW_TYPE_ITEM:
|
||||||
|
return onCreateItemViewHolder(parent, viewType);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract SH onCreateSectionViewHolder(ViewGroup parent);
|
||||||
|
protected abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType);
|
||||||
|
|
||||||
|
protected abstract void onBindSectionViewHolder(SH holder, int sectionIndex, T section);
|
||||||
|
protected abstract void onBindItemViewHolder(VH holder, Cursor cursor);
|
||||||
|
|
||||||
|
public interface Comparator<T> {
|
||||||
|
boolean equal(T obj1, T obj2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,12 +12,10 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<se.emilsjolander.stickylistheaders.StickyListHeadersListView
|
<android.support.v7.widget.RecyclerView
|
||||||
android:id="@+id/key_list_list"
|
android:id="@+id/key_list_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:drawSelectorOnTop="true"
|
|
||||||
android:fastScrollEnabled="true"
|
|
||||||
android:paddingLeft="16dp"
|
android:paddingLeft="16dp"
|
||||||
android:paddingRight="32dp"
|
android:paddingRight="32dp"
|
||||||
android:scrollbarStyle="outsideOverlay" />
|
android:scrollbarStyle="outsideOverlay" />
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:super="http://schemas.android.com/apk/lib-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" >
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
|
||||||
|
super:slm_headerDisplay="sticky|inline"
|
||||||
|
super:slm_section_sectionManager="linear"
|
||||||
|
tools:ignore="ResAuto">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="@style/SectionHeader"
|
style="@style/SectionHeader"
|
||||||
|
|||||||
Reference in New Issue
Block a user