From da543345ee43b906eab17c0e2a2ff7ca6743747c Mon Sep 17 00:00:00 2001 From: Tobias Erthal Date: Sun, 25 Sep 2016 17:26:51 +0200 Subject: [PATCH] Reintroduce changes made associated with KeyListFragment to new branch. --- OpenKeychain/build.gradle | 2 + .../keychain/matcher/CustomMatchers.java | 18 + .../keychain/ui/EditKeyTest.java | 19 +- .../keychain/ui/MiscCryptOperationTests.java | 15 +- .../keychain/ui/KeyListFragment.java | 519 ++++------------ .../keychain/ui/adapter/KeyAdapter.java | 23 - .../ui/adapter/KeySectionedListAdapter.java | 553 ++++++++++++++++++ .../ui/util/adapter/CursorAdapter.java | 437 ++++++++++++++ .../ui/util/adapter/SectionCursorAdapter.java | 318 ++++++++++ .../util/recyclerview/RecyclerFragment.java | 414 +++++++++++++ .../fastscroll/FastScrollRecyclerView.java | 122 ++++ .../recyclerview/fastscroll/FastScroller.java | 268 +++++++++ .../res/drawable-v21/list_item_ripple.xml | 12 + .../main/res/drawable/list_item_ripple.xml | 5 + .../src/main/res/layout/key_list_dummy.xml | 47 ++ .../src/main/res/layout/key_list_fragment.xml | 215 ++++--- .../src/main/res/layout/key_list_header.xml | 27 - .../res/layout/key_list_header_private.xml | 34 ++ .../res/layout/key_list_header_public.xml | 22 + .../src/main/res/layout/key_list_item.xml | 67 +-- OpenKeychain/src/main/res/values/attr.xml | 10 + OpenKeychain/src/main/res/values/colors.xml | 3 + 22 files changed, 2547 insertions(+), 603 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java 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/SectionCursorAdapter.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScrollRecyclerView.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScroller.java create mode 100644 OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml create mode 100644 OpenKeychain/src/main/res/drawable/list_item_ripple.xml create mode 100644 OpenKeychain/src/main/res/layout/key_list_dummy.xml delete mode 100644 OpenKeychain/src/main/res/layout/key_list_header.xml create mode 100644 OpenKeychain/src/main/res/layout/key_list_header_private.xml create mode 100644 OpenKeychain/src/main/res/layout/key_list_header_public.xml diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index e3919fae6..fc02ec683 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' @@ -131,6 +132,7 @@ dependencyVerification { 'com.android.support:support-vector-drawable:45b1f180b437a750429f6c1457181c167ba211c17fcb992f83cdbefef5eb1519', 'com.squareup.okio:okio:114bdc1f47338a68bcbc95abf2f5cdc72beeec91812f2fcd7b521c1937876266', 'com.fidesmo:nordpol-core:3de58e850a00bba5b4d3a604d1399bcd89f695ea191ec0b03a57222e18062d15', + 'com.tonicartos:superslim:ca89b5c674660cc6918a8f8fd385065bffeee27983e0d33c7c2f0ad7b34d2d49', ] } diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java index 6713cd237..c408c2266 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/matcher/CustomMatchers.java @@ -27,10 +27,13 @@ import android.view.View; import android.widget.ViewAnimator; import com.nispok.snackbar.Snackbar; + +import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter.KeyItem; +import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter; import org.sufficientlysecure.keychain.ui.widget.EncryptKeyCompletionView; import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; @@ -86,6 +89,21 @@ public abstract class CustomMatchers { }; } + public static Matcher withKeyHolderId(final long keyId) { + return new BoundedMatcher + (KeySectionedListAdapter.KeyItemViewHolder.class) { + @Override + public void describeTo(Description description) { + description.appendText("with ViewHolder id: " + keyId); + } + + @Override + protected boolean matchesSafely(KeySectionedListAdapter.KeyItemViewHolder item) { + return item.getItemId() == keyId; + } + }; + } + public static Matcher withKeyToken(@ColorRes final long keyId) { return new BoundedMatcher(EncryptKeyCompletionView.class) { public void describeTo(Description description) { diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/EditKeyTest.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/EditKeyTest.java index dbe487c7a..139967264 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/EditKeyTest.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/EditKeyTest.java @@ -20,20 +20,28 @@ package org.sufficientlysecure.keychain.ui; import android.app.Activity; import android.content.Intent; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.action.ViewActions; +import android.support.test.espresso.contrib.RecyclerViewActions; import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.rule.ActivityTestRule; +import android.support.v7.widget.RecyclerView; import android.widget.AdapterView; +import org.hamcrest.Description; +import org.hamcrest.Matcher; import org.junit.FixMethodOrder; import org.junit.Rule; import org.junit.runners.MethodSorters; import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.matcher.CustomMatchers; import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import static android.support.test.espresso.Espresso.onData; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnHolderItem; import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static android.support.test.espresso.matcher.ViewMatchers.withId; @@ -41,6 +49,7 @@ import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.CoreMatchers.allOf; import static org.sufficientlysecure.keychain.TestHelpers.checkSnackbar; import static org.sufficientlysecure.keychain.TestHelpers.importKeysFromResource; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyHolderId; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId; //TODO This test is disabled because it needs to be fixed to work with updated code @@ -70,10 +79,12 @@ public class EditKeyTest { importKeysFromResource(activity, "x.sec.asc"); // navigate to edit key dialog - onData(withKeyItemId(0x9D604D2F310716A3L)) - .inAdapterView(allOf(isAssignableFrom(AdapterView.class), - isDescendantOfA(ViewMatchers.withId(R.id.key_list_list)))) - .perform(click()); + onView(allOf( + isAssignableFrom(RecyclerView.class), + withId(android.R.id.list))) + .perform(actionOnHolderItem( + withKeyHolderId(0x9D604D2F310716A3L), click())); + onView(withId(R.id.view_key_card_user_ids_edit)).perform(click()); // no-op should yield snackbar diff --git a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/MiscCryptOperationTests.java b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/MiscCryptOperationTests.java index 816b538dd..784f26fe0 100644 --- a/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/MiscCryptOperationTests.java +++ b/OpenKeychain/src/androidTest/java/org/sufficientlysecure/keychain/ui/MiscCryptOperationTests.java @@ -27,8 +27,10 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build.VERSION_CODES; +import android.support.test.espresso.contrib.RecyclerViewActions; import android.support.test.espresso.intent.Intents; import android.support.test.espresso.intent.rule.IntentsTestRule; +import android.support.v7.widget.RecyclerView; import android.widget.AdapterView; import org.junit.Before; @@ -36,6 +38,7 @@ import org.junit.Rule; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.TestHelpers; +import org.sufficientlysecure.keychain.matcher.CustomMatchers; import org.sufficientlysecure.keychain.service.PassphraseCacheService; import org.sufficientlysecure.keychain.ui.util.Notify.Style; import org.sufficientlysecure.keychain.util.Preferences; @@ -49,6 +52,7 @@ import static android.support.test.espresso.Espresso.openActionBarOverflowOrOpti import static android.support.test.espresso.Espresso.pressBack; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnHolderItem; import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction; import static android.support.test.espresso.intent.matcher.IntentMatchers.hasCategories; import static android.support.test.espresso.intent.matcher.IntentMatchers.hasType; @@ -71,6 +75,7 @@ import static org.sufficientlysecure.keychain.TestHelpers.pickRandom; import static org.sufficientlysecure.keychain.TestHelpers.randomString; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.isRecyclerItemView; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withDisplayedChild; +import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyHolderId; import static org.sufficientlysecure.keychain.matcher.CustomMatchers.withKeyItemId; import static org.sufficientlysecure.keychain.matcher.DrawableMatcher.withDrawable; @@ -206,12 +211,12 @@ public class MiscCryptOperationTests { //@Test public void testEncryptTokenFromKeyView() throws Exception { + onView(allOf( + isAssignableFrom(RecyclerView.class), + withId(android.R.id.list))) + .perform(actionOnHolderItem( + withKeyHolderId(0x9D604D2F310716A3L), click())); - // navigate to edit key dialog - onData(withKeyItemId(0x9D604D2F310716A3L)) - .inAdapterView(allOf(isAssignableFrom(AdapterView.class), - isDescendantOfA(withId(R.id.key_list_list)))) - .perform(click()); onView(withId(R.id.view_key_action_encrypt_text)).perform(click()); // make sure the encrypt is correctly set 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..596a715af 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; @@ -45,15 +43,14 @@ 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; 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; @@ -70,26 +67,25 @@ 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.adapter.KeySectionedListAdapter; +import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter; +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; +import java.util.Arrays; /** * 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 - implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener, +public class KeyListFragment extends RecyclerFragment + implements SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks, FabContainer, CryptoOperationHelper.Callback { @@ -97,9 +93,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 StickyListHeadersListView mStickyList; - // saves the mode object for multiselect, needed for reset at some point private ActionMode mActionMode = null; @@ -117,16 +110,107 @@ public class KeyListFragment extends LoaderFragment // for ConsolidateOperation private CryptoOperationHelper mConsolidateOpHelper; + // Callbacks related to listview and menu events + private final ActionMode.Callback mActionCallback + = new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + getActivity().getMenuInflater().inflate(R.menu.key_list_multi, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_key_list_multi_encrypt: { + long[] keyIds = getAdapter().getSelectedMasterKeyIds(); + Intent intent = new Intent(getActivity(), EncryptFilesActivity.class); + intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA); + intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, keyIds); + + startActivityForResult(intent, REQUEST_ACTION); + mode.finish(); + break; + } + + case R.id.menu_key_list_multi_delete: { + long[] keyIds = getAdapter().getSelectedMasterKeyIds(); + boolean hasSecret = getAdapter().isAnySecretKeySelected(); + Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class); + intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, keyIds); + intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret); + if (hasSecret) { + intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER, + Preferences.getPreferences(getActivity()).getPreferredKeyserver()); + } + + startActivityForResult(intent, REQUEST_DELETE); + break; + } + } + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mActionMode = null; + if(getAdapter() != null) { + getAdapter().finishSelection(); + } + } + }; + + private final KeySectionedListAdapter.KeyListListener mKeyListener + = new KeySectionedListAdapter.KeyListListener() { + @Override + public void onKeyDummyItemClicked() { + createKey(); + } + + @Override + public void onKeyItemClicked(long masterKeyId) { + Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class); + viewIntent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId)); + startActivityForResult(viewIntent, REQUEST_VIEW_KEY); + } + + @Override + public void onSlingerButtonClicked(long masterKeyId) { + Intent safeSlingerIntent = new Intent(getActivity(), SafeSlingerActivity.class); + safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, masterKeyId); + startActivityForResult(safeSlingerIntent, REQUEST_ACTION); + } + + @Override + public void onSelectionStateChanged(int selectedCount) { + if(selectedCount < 1) { + if(mActionMode != null) { + mActionMode.finish(); + } + } else { + if(mActionMode == null) { + mActionMode = getActivity().startActionMode(mActionCallback); + } + + String keysSelected = getResources().getQuantityString( + R.plurals.key_list_selected_keys, selectedCount, selectedCount); + mActionMode.setTitle(keysSelected); + } + } + }; + + /** * 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 = (StickyListHeadersListView) 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); @@ -157,7 +241,7 @@ public class KeyListFragment extends LoaderFragment }); - return root; + return view; } /** @@ -170,102 +254,13 @@ 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); - - // Adds an empty footer view so that the Floating Action Button won't block content - // in last few rows. - View footer = new View(activity); - - int spacing = (int) android.util.TypedValue.applyDimension( - android.util.TypedValue.COMPLEX_UNIT_DIP, 72, getResources().getDisplayMetrics() - ); - - android.widget.AbsListView.LayoutParams params = new android.widget.AbsListView.LayoutParams( - android.widget.AbsListView.LayoutParams.MATCH_PARENT, - spacing - ); - - footer.setLayoutParams(params); - mStickyList.addFooterView(footer, null, false); - - /* - * Multi-selection - */ - mStickyList.setFastScrollAlwaysVisible(true); - - mStickyList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); - mStickyList.getWrappedList().setMultiChoiceModeListener(new MultiChoiceModeListener() { - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - android.view.MenuInflater inflater = activity.getMenuInflater(); - inflater.inflate(R.menu.key_list_multi, menu); - mActionMode = mode; - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - - // get IDs for checked positions as long array - long[] ids; - - switch (item.getItemId()) { - case R.id.menu_key_list_multi_encrypt: { - ids = mAdapter.getCurrentSelectedMasterKeyIds(); - encrypt(mode, ids); - break; - } - case R.id.menu_key_list_multi_delete: { - ids = mAdapter.getCurrentSelectedMasterKeyIds(); - showDeleteKeyDialog(ids, mAdapter.isAnySecretSelected()); - break; - } - } - return true; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - mActionMode = null; - mAdapter.clearSelection(); - } - - @Override - public void onItemCheckedStateChanged(ActionMode mode, int position, long id, - boolean checked) { - if (checked) { - mAdapter.setNewSelection(position, true); - } else { - mAdapter.removeSelection(position); - } - int count = mStickyList.getCheckedItemCount(); - String keysSelected = getResources().getQuantityString( - R.plurals.key_list_selected_keys, count, count); - mode.setTitle(keysSelected); - } - - }); - // We have a menu item to show in action bar. setHasOptionsMenu(true); // Start out with a progress indicator. - setContentShown(false); - - // this view is made visible if no data is available - mStickyList.setEmptyView(activity.findViewById(R.id.key_list_empty)); + hideList(false); // click on search button (in empty view) starts query for search string vSearchContainer = (ViewAnimator) activity.findViewById(R.id.search_container); @@ -278,8 +273,13 @@ public class KeyListFragment extends LoaderFragment }); // Create an empty adapter we will use to display the loaded data. - mAdapter = new KeyListAdapter(activity, null, 0); - mStickyList.setAdapter(mAdapter); + //mAdapter = new KeyListAdapter(activity, null, 0); + + KeySectionedListAdapter adapter = new KeySectionedListAdapter(getContext(), null); + adapter.setKeyListener(mKeyListener); + + setAdapter(adapter); + setLayoutManager(new LayoutManager(getActivity())); // Prepare the loader. Either re-connect with an existing one, // or start a new one. @@ -298,9 +298,6 @@ public class KeyListFragment extends LoaderFragment startActivity(searchIntent); } - static final String ORDER = - KeyRings.HAS_ANY_SECRET + " DESC, " + KeyRings.USER_ID + " COLLATE NOCASE ASC"; - @Override public Loader onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. This @@ -314,31 +311,17 @@ 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, + KeySectionedListAdapter.KeyListCursor.PROJECTION, null, null, + KeySectionedListAdapter.KeyListCursor.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); - - if (data != null && (mQuery == null || TextUtils.isEmpty(mQuery))) { - boolean isSecret = data.moveToFirst() && data.getInt(KeyListAdapter.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; - headerCursor.addRow(row); - - Cursor dataCursor = data; - data = new MergeCursor(new Cursor[] { - headerCursor, dataCursor - }); - } - } - mAdapter.swapCursor(data); + getAdapter().setSearchQuery(mQuery); + getAdapter().swapCursor(KeySectionedListAdapter.KeyListCursor.wrap(data)); // end action mode, if any if (mActionMode != null) { @@ -347,9 +330,9 @@ public class KeyListFragment extends LoaderFragment // The list should now be shown. if (isResumed()) { - setContentShown(true); + showList(true); } else { - setContentShownNoAnimation(true); + showList(false); } } @@ -358,47 +341,9 @@ 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); } - /** - * On click on item, start key view activity - */ - @Override - 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))); - startActivityForResult(viewIntent, REQUEST_VIEW_KEY); - } - - protected void encrypt(ActionMode mode, long[] masterKeyIds) { - Intent intent = new Intent(getActivity(), EncryptFilesActivity.class); - intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA); - intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, masterKeyIds); - // used instead of startActivity set actionbar based on callingPackage - startActivityForResult(intent, REQUEST_ACTION); - - mode.finish(); - } - - /** - * Show dialog to delete key - * - * @param hasSecret must contain whether the list of masterKeyIds contains a secret key or not - */ - public void showDeleteKeyDialog(long[] masterKeyIds, boolean hasSecret) { - Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class); - intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, masterKeyIds); - intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret); - if (hasSecret) { - intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER, - Preferences.getPreferences(getActivity()).getPreferredKeyserver()); - } - startActivityForResult(intent, REQUEST_DELETE); - } - - @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { inflater.inflate(R.menu.key_list, menu); @@ -413,7 +358,6 @@ public class KeyListFragment extends LoaderFragment // Get the searchview MenuItem searchItem = menu.findItem(R.id.menu_key_list_search); - SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); // Execute this when searching @@ -558,7 +502,6 @@ public class KeyListFragment extends LoaderFragment } ProviderHelper providerHelper = new ProviderHelper(activity); - Cursor cursor = providerHelper.getContentResolver().query( KeyRings.buildUnifiedKeyRingsUri(), new String[]{ KeyRings.FINGERPRINT @@ -573,7 +516,7 @@ public class KeyListFragment extends LoaderFragment ArrayList keyList = new ArrayList<>(); try { while (cursor.moveToNext()) { - byte[] blob = cursor.getBlob(0);//fingerprint column is 0 + byte[] blob = cursor.getBlob(0); //fingerprint column is 0 String fingerprint = KeyFormattingUtils.convertFingerprintToHex(blob); ParcelableKeyRing keyEntry = new ParcelableKeyRing(fingerprint, null); keyList.add(keyEntry); @@ -592,7 +535,6 @@ public class KeyListFragment extends LoaderFragment } private void consolidate() { - CryptoOperationHelper.Callback callback = new CryptoOperationHelper.Callback() { @@ -622,14 +564,11 @@ public class KeyListFragment extends LoaderFragment } }; - mConsolidateOpHelper = - new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing); - + mConsolidateOpHelper = new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing); mConsolidateOpHelper.cryptoOperation(); } private void benchmark() { - CryptoOperationHelper.Callback callback = new CryptoOperationHelper.Callback() { @@ -659,9 +598,7 @@ public class KeyListFragment extends LoaderFragment } }; - CryptoOperationHelper opHelper = - new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing); - + CryptoOperationHelper opHelper = new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing); opHelper.cryptoOperation(); } @@ -680,6 +617,7 @@ public class KeyListFragment extends LoaderFragment if (mActionMode != null) { mActionMode.finish(); } + if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) { OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT); result.createNotify(getActivity()).show(); @@ -751,209 +689,4 @@ public class KeyListFragment extends LoaderFragment public boolean onCryptoSetProgress(String msg, int progress, int max) { return false; } - - public class KeyListAdapter extends KeyAdapter implements StickyListHeadersAdapter { - - private HashMap mSelection = new HashMap<>(); - - private Context mContext; - - public KeyListAdapter(Context context, Cursor c, int flags) { - super(context, c, flags); - mContext = context; - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - View view = super.newView(context, cursor, parent); - - final KeyItemViewHolder holder = (KeyItemViewHolder) view.getTag(); - - holder.mSlinger.setVisibility(View.VISIBLE); - - ContentDescriptionHint.setup(holder.mSlingerButton); - holder.mSlingerButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (holder.mMasterKeyId != null) { - Intent safeSlingerIntent = new Intent(mContext, SafeSlingerActivity.class); - safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, holder.mMasterKeyId); - startActivityForResult(safeSlingerIntent, REQUEST_ACTION); - } - } - }); - - return view; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - // let the adapter handle setting up the row views - View v = super.getView(position, convertView, parent); - - int colorEmphasis = FormattingUtils.getColorFromAttr(mContext, R.attr.colorEmphasis); - - if (mSelection.get(position) != null) { - // selected position color - v.setBackgroundColor(colorEmphasis); - } else { - // default color - v.setBackgroundColor(Color.TRANSPARENT); - } - - return v; - } - - @Override - public void bindView(View view, Context context, Cursor cursor) { - boolean isSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0; - long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); - if (isSecret && masterKeyId == 0L) { - - // sort of a hack: if this item isn't enabled, we make it clickable - // to intercept its click events - view.setClickable(true); - - KeyItemViewHolder h = (KeyItemViewHolder) view.getTag(); - h.setDummy(new OnClickListener() { - @Override - public void onClick(View v) { - createKey(); - } - }); - return; - } - - super.bindView(view, context, cursor); - } - - private class HeaderViewHolder { - TextView mText; - 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); - holder.mText = (TextView) convertView.findViewById(R.id.stickylist_header_text); - holder.mCount = (TextView) convertView.findViewById(R.id.contacts_num); - convertView.setTag(holder); - } else { - holder = (HeaderViewHolder) convertView.getTag(); - } - - if (!mDataValid) { - // no data available at this point - Log.d(Constants.TAG, "getHeaderView: No data available at this point!"); - return convertView; - } - - if (!mCursor.moveToPosition(position)) { - throw new IllegalStateException("couldn't move cursor to position " + position); - } - - if (mCursor.getInt(INDEX_HAS_ANY_SECRET) != 0) { - { // set contact count - int num = mCursor.getCount(); - // If this is a dummy secret key, subtract one - if (mCursor.getLong(INDEX_MASTER_KEY_ID) == 0L) { - num -= 1; - } - String contactsTotal = mContext.getResources().getQuantityString(R.plurals.n_keys, num, num); - holder.mCount.setText(contactsTotal); - holder.mCount.setVisibility(View.VISIBLE); - } - - holder.mText.setText(convertView.getResources().getString(R.string.my_keys)); - return convertView; - } - - // set header text as first char in user id - String userId = mCursor.getString(INDEX_USER_ID); - String headerText = convertView.getResources().getString(R.string.user_id_no_name); - if (userId != null && userId.length() > 0) { - headerText = "" + userId.charAt(0); - } - holder.mText.setText(headerText); - holder.mCount.setVisibility(View.GONE); - 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) { - // no data available at this point - Log.d(Constants.TAG, "getHeaderView: No data available at this point!"); - return -1; - } - - if (!mCursor.moveToPosition(position)) { - throw new IllegalStateException("couldn't move cursor to position " + position); - } - - // early breakout: all secret keys are assigned id 0 - if (mCursor.getInt(INDEX_HAS_ANY_SECRET) != 0) { - return 1L; - } - // otherwise, return the first character of the name as ID - String userId = mCursor.getString(INDEX_USER_ID); - if (userId != null && userId.length() > 0) { - return Character.toUpperCase(userId.charAt(0)); - } else { - return Long.MAX_VALUE; - } - } - - /** - * -------------------------- MULTI-SELECTION METHODS -------------- - */ - public void setNewSelection(int position, boolean value) { - mSelection.put(position, value); - notifyDataSetChanged(); - } - - public boolean isAnySecretSelected() { - for (int pos : mSelection.keySet()) { - if (isSecretAvailable(pos)) { - return true; - } - } - return false; - } - - public long[] getCurrentSelectedMasterKeyIds() { - long[] ids = new long[mSelection.size()]; - int i = 0; - // get master key ids - for (int pos : mSelection.keySet()) { - ids[i++] = getMasterKeyId(pos); - } - return ids; - } - - public void removeSelection(int position) { - mSelection.remove(position); - notifyDataSetChanged(); - } - - public void clearSelection() { - mSelection.clear(); - notifyDataSetChanged(); - } - - } - } 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/adapter/KeySectionedListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java new file mode 100644 index 000000000..1ae5ae61a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeySectionedListAdapter.java @@ -0,0 +1,553 @@ +package org.sufficientlysecure.keychain.ui.adapter; + +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.MergeCursor; +import android.graphics.PorterDuff; +import android.support.v4.content.ContextCompat; +import android.text.TextUtils; +import android.text.format.DateUtils; +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.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; +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; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class KeySectionedListAdapter extends SectionCursorAdapter { + + 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; + + private String mQuery; + private List mSelected; + private KeyListListener mListener; + + private boolean mHasDummy = false; + + public KeySectionedListAdapter(Context context, Cursor cursor) { + super(context, KeyListCursor.wrap(cursor, KeyListCursor.class), 0); + + mQuery = ""; + mSelected = new ArrayList<>(); + } + + public void setSearchQuery(String query) { + mQuery = query; + } + + + @Override + public void onContentChanged() { + mHasDummy = false; + mSelected.clear(); + + if(mListener != null) { + mListener.onSelectionStateChanged(0); + } + + super.onContentChanged(); + } + + @Override + public KeyListCursor swapCursor(KeyListCursor cursor) { + if (cursor != null && (mQuery == null || TextUtils.isEmpty(mQuery))) { + boolean isSecret = cursor.moveToFirst() && cursor.isSecret(); + + if (!isSecret) { + MatrixCursor headerCursor = new MatrixCursor(KeyListCursor.PROJECTION); + Long[] row = new Long[KeyListCursor.PROJECTION.length]; + row[cursor.getColumnIndex(KeychainContract.KeyRings.HAS_ANY_SECRET)] = 1L; + row[cursor.getColumnIndex(KeychainContract.KeyRings.MASTER_KEY_ID)] = 0L; + headerCursor.addRow(row); + + Cursor[] toMerge = { + headerCursor, + cursor.getWrappedCursor() + }; + + cursor = KeyListCursor.wrap(new MergeCursor(toMerge)); + } + } + + return super.swapCursor(cursor); + } + + public void setKeyListener(KeyListListener listener) { + mListener = listener; + } + + private int getSelectedCount() { + return mSelected.size(); + } + + private void selectPosition(int position) { + mSelected.add(position); + notifyItemChanged(position); + } + + private void deselectPosition(int position) { + mSelected.remove(Integer.valueOf(position)); + notifyItemChanged(position); + } + + private boolean isSelected(int position) { + return mSelected.contains(position); + } + + public long[] getSelectedMasterKeyIds() { + long[] keys = new long[mSelected.size()]; + for(int i = 0; i < keys.length; i++) { + int index = getCursorPositionWithoutSections(mSelected.get(i)); + if(!moveCursor(index)) { + return keys; + } + + keys[i] = getIdFromCursor(getCursor()); + } + + return keys; + } + + public boolean isAnySecretKeySelected() { + for(int i = 0; i < mSelected.size(); i++) { + int index = getCursorPositionWithoutSections(mSelected.get(i)); + if(!moveCursor(index)) { + return false; + } + + if(getCursor().isSecret()) { + return true; + } + } + + return false; + } + + + /** + * Returns the number of database entries displayed. + * @return The item count + */ + public int getCount() { + if (getCursor() != null) { + return getCursor().getCount() - (mHasDummy ? 1 : 0); + } else { + return 0; + } + } + + @Override + public long getIdFromCursor(KeyListCursor cursor) { + return cursor.getKeyId(); + } + + @Override + protected Character getSectionFromCursor(KeyListCursor cursor) throws IllegalStateException { + if (cursor.isSecret()) { + if (cursor.getKeyId() == 0L) { + mHasDummy = true; + } + + return '#'; + } else { + String userId = cursor.getRawUserId(); + if(TextUtils.isEmpty(userId)) { + return '?'; + } else { + return Character.toUpperCase(userId.charAt(0)); + } + } + } + + @Override + 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)) { + KeyListCursor c = getCursor(); + + if (c.isSecret() && c.getKeyId() == 0L) { + 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, KeyListCursor cursor) { + if (holder.getItemViewTypeWithoutSections() == VIEW_ITEM_TYPE_KEY) { + Highlighter highlighter = new Highlighter(getContext(), mQuery); + ((KeyItemViewHolder) holder).bindKey(cursor, highlighter); + } + } + + public void finishSelection() { + Integer[] selected = mSelected.toArray( + new Integer[mSelected.size()] + ); + + mSelected.clear(); + + for(int i = 0; i < selected.length; i++) { + notifyItemChanged(selected[i]); + } + } + + private class KeyDummyViewHolder extends SectionCursorAdapter.ViewHolder + implements View.OnClickListener{ + + KeyDummyViewHolder(View itemView) { + super(itemView); + + itemView.setClickable(true); + itemView.setOnClickListener(this); + itemView.setEnabled(getSelectedCount() == 0); + } + + @Override + public void onClick(View view) { + if(mListener != null) { + mListener.onKeyDummyItemClicked(); + } + } + } + + public class KeyItemViewHolder extends SectionCursorAdapter.ViewHolder + implements View.OnClickListener, View.OnLongClickListener { + + private TextView mMainUserId; + private TextView mMainUserIdRest; + private TextView mCreationDate; + private ImageView mStatus; + private View mSlinger; + private ImageButton mSlingerButton; + + KeyItemViewHolder(View itemView) { + super(itemView); + + 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); + + itemView.setClickable(true); + itemView.setLongClickable(true); + itemView.setOnClickListener(this); + itemView.setOnLongClickListener(this); + + mSlingerButton.setClickable(true); + mSlingerButton.setOnClickListener(this); + } + + void bindKey(KeyListCursor keyItem, Highlighter highlighter) { + itemView.setSelected(isSelected(getAdapterPosition())); + Context context = itemView.getContext(); + + { // set name and stuff, common to both key types + OpenPgpUtils.UserId userIdSplit = keyItem.getUserId(); + 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); + } + } + + { // set edit button and status, specific by key type. Note: order is important! + int textColor; + if (keyItem.isRevoked()) { + KeyFormattingUtils.setStatusImage( + context, + mStatus, + null, + KeyFormattingUtils.State.REVOKED, + R.color.key_flag_gray + ); + + mStatus.setVisibility(View.VISIBLE); + mSlinger.setVisibility(View.GONE); + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else if (keyItem.isExpired()) { + KeyFormattingUtils.setStatusImage( + context, + mStatus, + null, + KeyFormattingUtils.State.EXPIRED, + R.color.key_flag_gray + ); + + mStatus.setVisibility(View.VISIBLE); + mSlinger.setVisibility(View.GONE); + textColor = ContextCompat.getColor(context, R.color.key_flag_gray); + } else if (keyItem.isSecret()) { + mStatus.setVisibility(View.GONE); + if (mSlingerButton.hasOnClickListeners()) { + mSlingerButton.setColorFilter( + FormattingUtils.getColorFromAttr(context, R.attr.colorTertiaryText), + PorterDuff.Mode.SRC_IN + ); + + mSlinger.setVisibility(View.VISIBLE); + } else { + mSlinger.setVisibility(View.GONE); + } + textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); + } else { + // this is a public key - show if it's verified + if (keyItem.isVerified()) { + KeyFormattingUtils.setStatusImage( + context, + mStatus, + KeyFormattingUtils.State.VERIFIED + ); + + mStatus.setVisibility(View.VISIBLE); + } else { + KeyFormattingUtils.setStatusImage( + context, + mStatus, + KeyFormattingUtils.State.UNVERIFIED + ); + + mStatus.setVisibility(View.VISIBLE); + } + mSlinger.setVisibility(View.GONE); + textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText); + } + + mMainUserId.setTextColor(textColor); + mMainUserIdRest.setTextColor(textColor); + + if (keyItem.hasDuplicate()) { + String dateTime = DateUtils.formatDateTime(context, + keyItem.getCreationTime(), + DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR + | DateUtils.FORMAT_ABBREV_MONTH); + mCreationDate.setText(context.getString(R.string.label_key_created, + dateTime)); + mCreationDate.setTextColor(textColor); + mCreationDate.setVisibility(View.VISIBLE); + } else { + mCreationDate.setVisibility(View.GONE); + } + + } + } + + @Override + public void onClick(View v) { + int pos = getAdapterPosition(); + switch (v.getId()) { + case R.id.key_list_item_slinger_button: + if (mListener != null) { + mListener.onSlingerButtonClicked(getItemId()); + } + break; + + default: + if (getSelectedCount() == 0) { + if (mListener != null) { + mListener.onKeyItemClicked(getItemId()); + } + } else { + if (isSelected(pos)) { + deselectPosition(pos); + } else { + selectPosition(pos); + } + + if (mListener != null) { + mListener.onSelectionStateChanged(getSelectedCount()); + } + } + break; + } + + } + + @Override + public boolean onLongClick(View v) { + System.out.println("Long Click!"); + if (getSelectedCount() == 0) { + selectPosition(getAdapterPosition()); + + if (mListener != null) { + mListener.onSelectionStateChanged(getSelectedCount()); + } + return true; + } + + return false; + } + } + + static class KeyHeaderViewHolder extends SectionCursorAdapter.ViewHolder { + private TextView mText1; + + public KeyHeaderViewHolder(View itemView) { + super(itemView); + mText1 = (TextView) itemView.findViewById(android.R.id.text1); + } + + public void bind(String title) { + mText1.setText(title); + } + } + + public static class KeyListCursor extends CursorAdapter.KeyCursor { + public static final String ORDER = KeychainContract.KeyRings.HAS_ANY_SECRET + + " DESC, " + KeychainContract.KeyRings.USER_ID + " COLLATE NOCASE ASC"; + + public static final String[] PROJECTION; + + static { + ArrayList arr = new ArrayList<>(); + arr.addAll(Arrays.asList(KeyCursor.PROJECTION)); + arr.addAll(Arrays.asList( + KeychainContract.KeyRings.VERIFIED, + KeychainContract.KeyRings.HAS_ANY_SECRET, + KeychainContract.KeyRings.FINGERPRINT, + KeychainContract.KeyRings.HAS_ENCRYPT + )); + + PROJECTION = arr.toArray(new String[arr.size()]); + } + + public static KeyListCursor wrap(Cursor cursor) { + if (cursor != null) { + return new KeyListCursor(cursor); + } else { + return null; + } + } + + private KeyListCursor(Cursor cursor) { + super(cursor); + } + + public boolean hasEncrypt() { + int index = getColumnIndexOrThrow(KeychainContract.KeyRings.HAS_ENCRYPT); + return getInt(index) != 0; + } + + public byte[] getRawFingerprint() { + int index = getColumnIndexOrThrow(KeychainContract.KeyRings.FINGERPRINT); + return getBlob(index); + } + + public String getFingerprint() { + return KeyFormattingUtils.convertFingerprintToHex(getRawFingerprint()); + } + + public boolean isSecret() { + int index = getColumnIndexOrThrow(KeychainContract.KeyRings.HAS_ANY_SECRET); + return getInt(index) != 0; + } + + public boolean isVerified() { + int index = getColumnIndexOrThrow(KeychainContract.KeyRings.VERIFIED); + return getInt(index) > 0; + } + } + + public interface KeyListListener { + void onKeyDummyItemClicked(); + void onKeyItemClicked(long masterKeyId); + void onSlingerButtonClicked(long masterKeyId); + void onSelectionStateChanged(int selectedCount); + } +} 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..511696d24 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/CursorAdapter.java @@ -0,0 +1,437 @@ +package org.sufficientlysecure.keychain.ui.util.adapter; + +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.CursorWrapper; +import android.database.DataSetObserver; +import android.os.Handler; +import android.support.v7.widget.RecyclerView; + +import org.openintents.openpgp.util.OpenPgpUtils; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.pgp.KeyRing; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.util.Log; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; + +public abstract class CursorAdapter + extends RecyclerView.Adapter { + public static final String TAG = "CursorAdapter"; + + private C 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, AbstractCursor, 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, C c) { + setHasStableIds(true); + 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, C c, int flags) { + setHasStableIds(true); + init(context, c, flags); + } + + private void init(Context context, C 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 C 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(C cursor) { + if(cursor != null) { + return cursor.getEntryId(); + } 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(C cursor) { + Cursor old = swapCursor(cursor); + if (old != null) { + old.close(); + } + } + + /** + * Swap in a new Cursor, returning the old Cursor. Unlike + * {@link #changeCursor(AbstractCursor)}, 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 C swapCursor(C newCursor) { + if (newCursor == mCursor) { + return null; + } + + C 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(); + } + } + + public static abstract class AbstractCursor extends CursorWrapper { + public static final String[] PROJECTION = { "_id" }; + + public static T wrap(Cursor cursor, Class type) { + if (cursor != null) { + try { + Constructor constructor = type.getConstructor(Cursor.class); + return constructor.newInstance(cursor); + } catch (Exception e) { + Log.e(Constants.TAG, "Could not create instance of cursor wrapper!", e); + } + } + + return null; + } + + private HashMap mColumnIndices; + + /** + * Creates a cursor wrapper. + * + * @param cursor The underlying cursor to wrap. + */ + protected AbstractCursor(Cursor cursor) { + super(cursor); + mColumnIndices = new HashMap<>(cursor.getColumnCount() * 4 / 3, 0.75f); + } + + @Override + public void close() { + mColumnIndices.clear(); + super.close(); + } + + public final int getEntryId() { + int index = getColumnIndexOrThrow("_id"); + return getInt(index); + } + + @Override + public final int getColumnIndexOrThrow(String colName) { + Integer colIndex = mColumnIndices.get(colName); + if(colIndex == null) { + colIndex = super.getColumnIndexOrThrow(colName); + mColumnIndices.put(colName, colIndex); + } else if (colIndex < 0){ + throw new IllegalArgumentException("Could not get column index for name: \"" + colName + "\""); + } + + return colIndex; + } + + @Override + public final int getColumnIndex(String colName) { + Integer colIndex = mColumnIndices.get(colName); + if(colIndex == null) { + colIndex = super.getColumnIndex(colName); + mColumnIndices.put(colName, colIndex); + } + + return colIndex; + } + } + + public static class KeyCursor extends AbstractCursor { + public static final String[] PROJECTION; + + static { + ArrayList arr = new ArrayList<>(); + arr.addAll(Arrays.asList(AbstractCursor.PROJECTION)); + arr.addAll(Arrays.asList( + KeychainContract.KeyRings.MASTER_KEY_ID, + KeychainContract.KeyRings.USER_ID, + KeychainContract.KeyRings.IS_REVOKED, + KeychainContract.KeyRings.IS_EXPIRED, + KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID, + KeychainContract.KeyRings.CREATION + )); + + PROJECTION = arr.toArray(new String[arr.size()]); + } + + public static KeyCursor wrap(Cursor cursor) { + if (cursor != null) { + return new KeyCursor(cursor); + } else { + return null; + } + } + + /** + * Creates a cursor wrapper. + * + * @param cursor The underlying cursor to wrap. + */ + protected KeyCursor(Cursor cursor) { + super(cursor); + } + + public long getKeyId() { + int index = getColumnIndexOrThrow(KeychainContract.KeyRings.MASTER_KEY_ID); + return getLong(index); + } + + public String getRawUserId() { + int index = getColumnIndexOrThrow(KeychainContract.KeyRings.USER_ID); + return getString(index); + } + + public OpenPgpUtils.UserId getUserId() { + return KeyRing.splitUserId(getRawUserId()); + } + + public boolean hasDuplicate() { + int index = getColumnIndexOrThrow(KeychainContract.KeyRings.HAS_DUPLICATE_USER_ID); + return getLong(index) > 0L; + } + + public boolean isRevoked() { + int index = getColumnIndexOrThrow(KeychainContract.KeyRings.IS_REVOKED); + return getInt(index) > 0; + } + + public boolean isExpired() { + int index = getColumnIndexOrThrow(KeychainContract.KeyRings.IS_EXPIRED); + return getInt(index) > 0; + } + + public long getCreationTime() { + int index = getColumnIndexOrThrow(KeychainContract.KeyRings.CREATION); + return getLong(index) * 1000; + } + + public Date getCreationDate() { + return new Date(getCreationTime()); + } + } +} \ No newline at end of file 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..2fe53652d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/adapter/SectionCursorAdapter.java @@ -0,0 +1,318 @@ +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.View; +import android.view.ViewGroup; + +import com.tonicartos.superslim.LayoutManager; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.Objects; + +/** + * @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 { + + public static final String TAG = "SectionCursorAdapter"; + + private static final short VIEW_TYPE_ITEM = 0x1; + private static final short VIEW_TYPE_SECTION = 0x2; + + private SparseArrayCompat mSectionMap = new SparseArrayCompat<>(); + private Comparator mSectionComparator; + + public SectionCursorAdapter(Context context, C 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, C cursor, int flags, Comparator comparator) { + super(context, cursor, flags); + setSectionComparator(comparator); + } + + @Override + public void onContentChanged() { + if (hasValidData()) { + buildSections(); + } else { + mSectionMap.clear(); + } + + 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(); + 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(); + } + } + } + + private void appendSections(C 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(C cursor) throws IllegalStateException; + + @Override + public int getItemCount() { + return super.getItemCount() + mSectionMap.size(); + } + + @Override + public final long getItemId(int listPosition) { + /* + int index = mSectionMap.indexOfKey(listPosition); + if (index < 0) { + int cursorPosition = getCursorPositionWithoutSections(listPosition); + return super.getItemId(cursorPosition); + } else { + T section = mSectionMap.valueAt(index); + return section != null ? section.hashCode() : 0L; + } */ + + if (isSection(listPosition)) { + return RecyclerView.NO_ID; + } 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; + } + } + + public int getListPosition(int cursorPosition) { + for(int i = 0; i < mSectionMap.size(); i++) { + int sectionIndex = mSectionMap.keyAt(i); + if (sectionIndex > cursorPosition) { + return cursorPosition; + } + + cursorPosition +=1; + } + + return cursorPosition; + } + + /** + * 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; + } + + + 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); + } + + 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) { + int sectionIndex = mSectionMap.indexOfKey(listPosition); + if(sectionIndex < 0) { + int cursorPosition = getCursorPositionWithoutSections(listPosition); + return (getSectionItemViewType(cursorPosition) << 16) | VIEW_TYPE_ITEM; + } else { + return (getSectionHeaderViewType(sectionIndex) << 16) | VIEW_TYPE_SECTION; + } + } + + protected short getSectionHeaderViewType(int sectionIndex) { + return 0; + } + + protected short getSectionItemViewType(int position) { + return 0; + } + + @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)); + + int viewType = holder.getItemViewType() & 0xFF; + switch (viewType) { + case VIEW_TYPE_ITEM : + moveCursorOrThrow(getCursorPositionWithoutSections(position)); + onBindItemViewHolder((VH) holder, getCursor()); + + layoutParams.isHeader = false; + break; + + case VIEW_TYPE_SECTION: + T section = mSectionMap.get(position); + onBindSectionViewHolder((SH) holder, section); + + layoutParams.isHeader = true; + break; + } + + holder.itemView.setLayoutParams(layoutParams); + } + + @Override + public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType & 0xFF) { + case VIEW_TYPE_SECTION: + return onCreateSectionViewHolder(parent, viewType >> 16); + + case VIEW_TYPE_ITEM: + return onCreateItemViewHolder(parent, viewType >> 16); + + default: + return null; + } + } + + protected abstract SH onCreateSectionViewHolder(ViewGroup parent, int viewType); + protected abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType); + + protected abstract void onBindSectionViewHolder(SH holder, T section); + protected abstract void onBindItemViewHolder(VH holder, C 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..03bb52c15 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/RecyclerFragment.java @@ -0,0 +1,414 @@ +/* + * 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.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; +import org.sufficientlysecure.keychain.util.Log; + +public class RecyclerFragment extends Fragment { + protected static final int INTERNAL_LIST_VIEW_ID = android.R.id.list; + protected static final int INTERNAL_EMPTY_VIEW_ID = android.R.id.empty; + protected static final int INTERNAL_LIST_CONTAINER_ID = android.R.id.widget_frame; + protected 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 = 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); + + int padding = FormattingUtils.dpToPx(context, 8); + listView.setPadding(padding, 0, padding, 0); + listView.setClipToPadding(false); + + 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); + } + + public void setEmptyText(String text) { + ensureList(); + if(emptyView instanceof TextView) { + ((TextView) emptyView).setText(text); + } else { + Log.e(Constants.TAG, "Cannot set empty text on a view that is null" + + "or not an instance of android.view.View!"); + } + } + + 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/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScrollRecyclerView.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScrollRecyclerView.java new file mode 100644 index 000000000..7b1c7a388 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScrollRecyclerView.java @@ -0,0 +1,122 @@ +package org.sufficientlysecure.keychain.ui.util.recyclerview.fastscroll; + +import android.content.Context; +import android.graphics.Canvas; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import com.tonicartos.superslim.LayoutManager; + +public class FastScrollRecyclerView extends RecyclerView implements RecyclerView.OnItemTouchListener { + private FastScroller mFastScroller; + + private int mLastX; + private int mLastY; + + public FastScrollRecyclerView(Context context) { + this(context, null); + } + + public FastScrollRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FastScrollRecyclerView(Context context, AttributeSet attributeSet, int defStyleAttr) { + super(context, attributeSet, defStyleAttr); + mFastScroller = new FastScroller(this, attributeSet); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + addOnItemTouchListener(this); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + mFastScroller.draw(canvas); + } + + @Override + public void onScrolled(int x, int y) { + super.onScrolled(x, y); + mFastScroller.updateThumb( + computeVerticalScrollOffset(), + computeVerticalScrollExtent()); + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + mFastScroller.updateContainer(top, right, bottom); + mFastScroller.updateThumb( + computeVerticalScrollOffset(), + computeVerticalScrollExtent()); + } + + @Override + public void onScrollStateChanged(int state) { + switch (state) { + case SCROLL_STATE_IDLE: + mFastScroller.hideBar(); + break; + case SCROLL_STATE_DRAGGING: + mFastScroller.showBar(); + break; + } + } + + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { + if(mFastScroller.onInterceptTouchEvent(e)) { + onTouchEvent(rv, e); + return true; + } else { + return false; + } + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + mFastScroller.handleTouchEvent(event.getAction(), x, y, mLastX, mLastY); + mLastX = x; + mLastY = y; + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + + } + + private int getItemCount() { + return getAdapter() != null ? + getAdapter().getItemCount() : 0; + } + + public void scrollToFraction(float fraction) { + int count = getItemCount(); + if (count > 0) { + stopScroll(); + scrollToPosition((int) ((count - 1) * fraction)); + } + } + + public void scrollByFraction(float fraction) { + int count = getItemCount(); + if (count > 0) { + stopScroll(); + + + int pixelsToScroll = (int) (computeVerticalScrollRange() * fraction); + System.out.println("ScrollBy Fraction: " + fraction + ", Pixel: " + pixelsToScroll); + + scrollBy(0, pixelsToScroll); + } + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScroller.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScroller.java new file mode 100644 index 000000000..8b55221ac --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/util/recyclerview/fastscroll/FastScroller.java @@ -0,0 +1,268 @@ +package org.sufficientlysecure.keychain.ui.util.recyclerview.fastscroll; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.support.v4.view.animation.FastOutLinearInInterpolator; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; + +public class FastScroller { + private static final int DEFAULT_AUTO_HIDE_DELAY = 1500; + private static final boolean DEFAULT_AUTO_HIDE_ENABLED = true; + + private Paint mTrack; + private Paint mThumb; + + private int mPosX; + private int mPosY; + private int mWidth; + private int mHeight; + + private Rect mContainer; + private boolean mIsDragging; + + private int mAutoHideDelay; + private boolean mAutoHideEnabled; + + private final int mTrackColorNormal; + private final int mTrackColorDragging; + + private final int mThumbColorNormal; + private final int mThumbColorDragging; + + private ValueAnimator mBarAnimator; + private final FastScrollRecyclerView mRecyclerView; + + private final int mTouchSlop; + private final int mTouchInset; + private final int[] mKeyFrames; + + //private final Runnable mHideRunnable; + + public FastScroller(final FastScrollRecyclerView recyclerView, AttributeSet attributeSet) { + Context context = recyclerView.getContext(); + mRecyclerView = recyclerView; + + mTrack = new Paint(Paint.ANTI_ALIAS_FLAG); + mThumb = new Paint(Paint.ANTI_ALIAS_FLAG); + + mKeyFrames = new int[2]; + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mTouchInset = FormattingUtils.dpToPx(context, 8); + + TypedArray typedArray = context.getTheme().obtainStyledAttributes( + attributeSet, R.styleable.FastScroller, 0, 0); + + try { + mWidth = typedArray.getDimensionPixelSize( + R.styleable.FastScroller_fastScrollScrollBarWidth, FormattingUtils.dpToPx(context, 6)); + + mAutoHideDelay = typedArray.getInteger( + R.styleable.FastScroller_fastScrollAutoHideDelay, DEFAULT_AUTO_HIDE_DELAY); + mAutoHideEnabled = typedArray.getBoolean( + R.styleable.FastScroller_fastScrollAutoHideEnabled, DEFAULT_AUTO_HIDE_ENABLED); + + mTrackColorNormal = typedArray.getColor( + R.styleable.FastScroller_fastScrollTrackColorNormal, Color.LTGRAY); + mTrackColorDragging = typedArray.getColor( + R.styleable.FastScroller_fastScrollTrackColorDragging, Color.GRAY); + + mThumbColorNormal = typedArray.getColor( + R.styleable.FastScroller_fastScrollThumbColorNormal, Color.GRAY); + mThumbColorDragging = typedArray.getColor( + R.styleable.FastScroller_fastScrollThumbColorDragging, Color.BLUE); + + mTrack.setColor(mTrackColorNormal); + mThumb.setColor(mThumbColorNormal); + } finally { + typedArray.recycle(); + } + } + + public void hideBar() { + if(mPosX >= mWidth || !mAutoHideEnabled) { + return; + } + + mKeyFrames[0] = mPosX; + mKeyFrames[1] = mWidth; + + prepareAnimator(); + mBarAnimator.setIntValues(mKeyFrames); + mBarAnimator.setStartDelay(mAutoHideDelay); + mBarAnimator.setInterpolator(new FastOutLinearInInterpolator()); + mBarAnimator.start(); + } + + public void showBar() { + if(mPosX < 1) { + return; + } + + mKeyFrames[0] = mPosX; + mKeyFrames[1] = 0; + + prepareAnimator(); + mBarAnimator.setStartDelay(0); + mBarAnimator.setIntValues(mKeyFrames); + mBarAnimator.setInterpolator(new FastOutSlowInInterpolator()); + mBarAnimator.start(); + } + + public boolean isAnimatingShow() { + return mKeyFrames[1] == 0 && mBarAnimator.isStarted(); + } + + public boolean isAnimatingHide() { + return mKeyFrames[1] == mWidth && mBarAnimator.isStarted(); + } + + public boolean isBarFullyShown() { + return mPosX < 1; + } + + public boolean onInterceptTouchEvent(MotionEvent event) { + return mContainer != null + && (event.getX() < mContainer.right) + && (event.getX() > mContainer.left - mTouchInset) + && (event.getY() < mContainer.bottom) + && (event.getY() > mContainer.top); + } + + private boolean isInThumb(int x, int y) { + return x > (mContainer.left + mPosX - mTouchInset) + && x < (mContainer.right + mTouchInset) + && y > (mContainer.top + mPosY - mTouchInset) + && y < (mContainer.top + mPosY + mHeight + mTouchInset); + } + + public void draw(Canvas canvas) { + if(mPosX < 0 || mPosX >= mWidth) { + return; + } + + if(!mRecyclerView.canScrollVertically(-1) + && ! mRecyclerView.canScrollVertically(1)) { + return; + } + + int topBound = mContainer.top + mPosY; + int leftBound = mContainer.left + mPosX; + + canvas.drawRect(leftBound, mContainer.top, mContainer.right, mContainer.bottom, mTrack); + canvas.drawRect(leftBound, topBound, mContainer.right, topBound + mHeight, mThumb); + } + + public void updateThumb(int offset, int extent) { + mPosY = offset; + mHeight = extent; + } + + public void updateContainer(int top, int right, int bottom) { + mPosX = 0; + mContainer = new Rect(right - mWidth, top, right, bottom); + + invalidate(); + } + + private void invalidate() { + mRecyclerView.invalidate(mContainer); + } + + private void prepareAnimator() { + if (mBarAnimator != null) { + mBarAnimator.cancel(); + } else { + mBarAnimator = new ValueAnimator(); + mBarAnimator.setDuration(150); + mBarAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mPosX = (Integer) animation.getAnimatedValue(); + + invalidate(); + } + }); + } + } + + public void handleTouchEvent(int action, int x, int y, int lastX, int lastY) { + if(!mRecyclerView.canScrollVertically(-1) + && ! mRecyclerView.canScrollVertically(1)) { + return; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + if(isBarFullyShown()) { // + prepareAnimator(); // cancel any pending animations + + mTrack.setColor(mTrackColorDragging); + mThumb.setColor(mThumbColorDragging); + + if(!isInThumb(x, y)) { + // jump to point + mPosY = Math.min( + Math.max(mContainer.top, y - (mHeight / 2)), + mContainer.bottom - mHeight + ); + + float range = (mContainer.bottom - mContainer.top) - mHeight; + mRecyclerView.scrollToFraction(mPosY / range); + } else { + invalidate(); + } + } else { + if(!isAnimatingShow()) { + showBar(); + } + } + break; + case MotionEvent.ACTION_MOVE: + if((!mIsDragging + && isInThumb(x, y) + && Math.abs(y - lastY) > mTouchSlop) + || mIsDragging) { + + if(!mIsDragging) { + mIsDragging = true; + } + + float dist = y - lastY; + float range = (mContainer.bottom - mContainer.top) - mHeight; + + if(mRecyclerView.canScrollVertically(dist < 0 ? -1 : 1)) { + mRecyclerView.scrollByFraction(dist / range); + } + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if(mIsDragging) { + mIsDragging = false; + } + + mTrack.setColor(mTrackColorNormal); + mThumb.setColor(mThumbColorNormal); + + if(!mBarAnimator.isRunning() + && mRecyclerView.getScrollState() + == RecyclerView.SCROLL_STATE_IDLE) { + hideBar(); + invalidate(); + } + break; + } + } +} diff --git a/OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml b/OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml new file mode 100644 index 000000000..32d726ac1 --- /dev/null +++ b/OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/drawable/list_item_ripple.xml b/OpenKeychain/src/main/res/drawable/list_item_ripple.xml new file mode 100644 index 000000000..ae8972031 --- /dev/null +++ b/OpenKeychain/src/main/res/drawable/list_item_ripple.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file 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 6aaf5be25..05a32efaf 100644 --- a/OpenKeychain/src/main/res/layout/key_list_fragment.xml +++ b/OpenKeychain/src/main/res/layout/key_list_fragment.xml @@ -1,117 +1,144 @@ - + 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" > - - - - -