Merge branch 'tobsbot-recycler-view-keylist'

This commit is contained in:
Dominik Schürmann
2016-11-30 15:17:14 +01:00
20 changed files with 2211 additions and 631 deletions

View File

@@ -23,7 +23,6 @@ dependencies {
compile 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.0'
compile 'org.ocpsoft.prettytime:prettytime:4.0.1.Final'
compile 'com.splitwise:tokenautocomplete:2.0.8@aar'
compile 'se.emilsjolander:stickylistheaders:2.7.0'
compile 'org.sufficientlysecure:html-textview:2.0'
compile 'org.sufficientlysecure:donations:2.4'
compile 'com.nispok:snackbar:2.11.0'
@@ -34,6 +33,10 @@ dependencies {
compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0'
compile 'com.cocosw:bottomsheet:1.3.0@aar'
compile 'se.emilsjolander:stickylistheaders:2.7.0'
compile 'com.tonicartos:superslim:0.4.13'
compile 'com.futuremind.recyclerfastscroll:fastscroll:0.2.4'
// Material Drawer
compile 'com.mikepenz:materialdrawer:5.6.0@aar'
compile 'com.mikepenz:fastadapter:1.8.2'
@@ -101,7 +104,6 @@ dependencyVerification {
'org.commonjava.googlecode.markdown4j:markdown4j:e952e825d29e1317d96f79f346bfb6786c7c5eef50bd26e54a80823704b62e13',
'org.ocpsoft.prettytime:prettytime:ef7098d973ae78b57d1a22dc37d3b8a771bf030301300e24055d676b6cdc5e75',
'com.splitwise:tokenautocomplete:f921f83ee26b5265f719b312c30452ef8e219557826c5ce5bf02e29647967939',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
'org.sufficientlysecure:html-textview:302c449167f9573313e5293ccab689010e028e4d09aee2ccc2682b3211227ce7',
'org.sufficientlysecure:donations:96f8197bab26dfe41900d824f10f8f1914519cd62eedb77bdac5b223eccdf0a6',
'com.nispok:snackbar:46b5eb9d630d329e13c2ce00ee9fb115ffb66c23c72cff32ee97eedd76824c6f',
@@ -111,6 +113,9 @@ dependencyVerification {
'org.apache.james:apache-mime4j-dom:e18717fe6d36f32e5c5f7cbeea1a9bf04645fdabc84e7e8374d9da10fd52e78d',
'org.thoughtcrime.ssl.pinning:AndroidPinning:afa1d74e699257fa75cb109ff29bac50726ef269c6e306bdeffe8223cee06ef4',
'com.cocosw:bottomsheet:4af6112a7f4cad4e2b70e5fdf1edc39f51275523a0f53011a012837dc103e597',
'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb',
'com.tonicartos:superslim:ca89b5c674660cc6918a8f8fd385065bffeee27983e0d33c7c2f0ad7b34d2d49',
'com.futuremind.recyclerfastscroll:fastscroll:ae655201885a9dbb5fabecb4adfefbb23ffdbca26a2b4ea255ec1bf6f214c606',
'com.mikepenz:materialdrawer:8bba1428dcef5ad7c2decf49c612ad980b38e2f1031cbd66c152a8a104793929',
'com.mikepenz:fastadapter:21d4ecb5c128bcda37b14e7998d799ed52cfc768b72cdf3d5578bb6775769ebd',
'com.mikepenz:materialize:942ccf5e2aa1a46803aa884e8dc7bbaf2a9e8e9996a0cf92e3fe2f44a8592ba4',
@@ -119,14 +124,14 @@ dependencyVerification {
'com.mikepenz:fontawesome-typeface:ee47b7fe97b90412f01f2fcdd78f65a4edb0ab00006f5ef59ed00516baca9309',
'com.mikepenz:community-material-typeface:d6035d261c5eba880cd7fe5dcb8cc00b09bfe6d41063b881b759e9897dc7b7c9',
'com.fidesmo:nordpol-android:56f43fe2b1676817bcb4085926de14a08282ef6729c855c198d81aec62b20d65',
// 'OpenKeychain:openpgp-api-lib:43d7e67e8ae19ae46e228393a7c4f5861da64db47da2fbb398817bde774669f7',
// 'OpenKeychain:openkeychain-api-lib:d2e66ce4828c47cb6081cfb8e7352681cc5d337404aec87e156cbc5a6e732587',
// 'OpenKeychain.extern.bouncycastle:core:20bd3036f93758c5ed937fc172b5e7e8d82d071e7b435bd9b6fa3869d5788095',
// 'OpenKeychain.extern.bouncycastle:pg:97b7318ee208b8fe552425aac9ffdcc8723032a78784ecc82c999ee09161e2e8',
// 'OpenKeychain.extern.bouncycastle:prov:76a8d07e88a557f91fad5c01f1f827085bde12ee924fa4d186a65d80e39d42a2',
// 'OpenKeychain.extern:minidns:8183e688b754b9e64ea498b735890a3ece9af1e240d949b804bf7b12a0d5b830',
// 'OpenKeychain:KeybaseLib:a870229a22779dfe433d6e48970822b0dfcd351c3cc885cfb94816d064c10fb0',
// 'OpenKeychain:safeslinger-exchange:2cbff71ec5d59c824dd244042539cfc5cfbf450ab813c95b9dc83fbb50401ba9',
// 'OpenKeychain:openpgp-api-lib:8ee931094b4c596fce55907b226616b0adae614b2877a0e530f726898bf63259',
// 'OpenKeychain:openkeychain-api-lib:a61e822cca9f7b34ea7750532ecd3bad7e05e6872bf218583f2a25d98c8b3c68',
// 'OpenKeychain.extern.bouncycastle:core:72c07a72f304b9ff96bc4027861df5adbfbeb5dfccc740198c1d5de393980d8b',
// 'OpenKeychain.extern.bouncycastle:pg:f61645e289fdbcc38b24dc2c5e9186eb9460d3eb76b7607e2dd68ed3f2115202',
// 'OpenKeychain.extern.bouncycastle:prov:2dc9e69d1a2a91096716af024b02d1e29af19243127eae71f00a957be52d7f69',
// 'OpenKeychain.extern:minidns:5349807fe5b2134862ad05cadc8be3fd3fb7aa55c2b129d7ba8f4904faf3333b',
// 'OpenKeychain:KeybaseLib:75c70462e244d99c647da6638dea4b790455fb9f539574398ea9da7571f5ee8f',
// 'OpenKeychain:safeslinger-exchange:be515e3012549b07bfdf779a262d1f8bb335a98c35c059db6563c1d0b58bde75',
// 'com.android.databinding:library:def2976cb30dd5abf9f3a35d70c70cfb5485af4fb4ae022f5b9a6e2f8cff6386',
// 'com.android.databinding:baseLibrary:47cb0d2d4d1aae4af3f860c31540493332a26278c016bbae90d22fdde3b0b83d',
// 'com.android.databinding:adapters:0dd06349dad760f3cb56769f8e9f46451634be6a8a1bfdb2e88a5ca10afcebd6',

View File

@@ -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<RecyclerView.ViewHolder> withKeyHolderId(final long keyId) {
return new BoundedMatcher<RecyclerView.ViewHolder, KeySectionedListAdapter.KeyItemViewHolder>
(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<View> withKeyToken(@ColorRes final long keyId) {
return new BoundedMatcher<View, EncryptKeyCompletionView>(EncryptKeyCompletionView.class) {
public void describeTo(Description description) {

View File

@@ -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

View File

@@ -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

View File

@@ -21,12 +21,8 @@ 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,18 +41,17 @@ 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.futuremind.recyclerviewfastscroll.FastScroller;
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.ParcelableHkpKeyserver;
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
import org.sufficientlysecure.keychain.operations.results.BenchmarkResult;
import org.sufficientlysecure.keychain.operations.results.ConsolidateResult;
@@ -69,30 +64,20 @@ import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.BenchmarkInputParcel;
import org.sufficientlysecure.keychain.service.ConsolidateInputParcel;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerFragment;
import org.sufficientlysecure.keychain.util.FabContainer;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.keyimport.ParcelableHkpKeyserver;
import org.sufficientlysecure.keychain.util.Preferences;
import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Public key list with sticky list headers. It does _not_ extend ListFragment because it uses
* StickyListHeaders library which does not extend upon ListView.
*/
public class KeyListFragment extends LoaderFragment
implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener,
public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
implements SearchView.OnQueryTextListener,
LoaderManager.LoaderCallbacks<Cursor>, FabContainer,
CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> {
@@ -100,9 +85,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;
@@ -120,16 +102,107 @@ public class KeyListFragment extends LoaderFragment
// for ConsolidateOperation
private CryptoOperationHelper<ConsolidateInputParcel, ConsolidateResult> 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);
@@ -160,115 +233,25 @@ public class KeyListFragment extends LoaderFragment
});
return root;
return view;
}
/**
* Define Adapter and Loader on create of Activity
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 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);
@@ -280,9 +263,14 @@ 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);
KeySectionedListAdapter adapter = new KeySectionedListAdapter(getContext(), null);
adapter.setKeyListener(mKeyListener);
setAdapter(adapter);
setLayoutManager(new LayoutManager(getActivity()));
FastScroller fastScroller = (FastScroller) getActivity().findViewById(R.id.fastscroll);
fastScroller.setRecyclerView(getRecyclerView());
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
@@ -301,9 +289,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<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
@@ -317,31 +302,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<Cursor> 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) {
@@ -350,9 +321,9 @@ public class KeyListFragment extends LoaderFragment
// The list should now be shown.
if (isResumed()) {
setContentShown(true);
showList(true);
} else {
setContentShownNoAnimation(true);
showList(false);
}
}
@@ -361,47 +332,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);
@@ -416,7 +349,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
@@ -561,7 +493,6 @@ public class KeyListFragment extends LoaderFragment
}
ProviderHelper providerHelper = new ProviderHelper(activity);
Cursor cursor = providerHelper.getContentResolver().query(
KeyRings.buildUnifiedKeyRingsUri(), new String[]{
KeyRings.FINGERPRINT
@@ -576,7 +507,7 @@ public class KeyListFragment extends LoaderFragment
ArrayList<ParcelableKeyRing> 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, null, null);
keyList.add(keyEntry);
@@ -595,7 +526,6 @@ public class KeyListFragment extends LoaderFragment
}
private void consolidate() {
CryptoOperationHelper.Callback<ConsolidateInputParcel, ConsolidateResult> callback
= new CryptoOperationHelper.Callback<ConsolidateInputParcel, ConsolidateResult>() {
@@ -625,14 +555,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<BenchmarkInputParcel, BenchmarkResult> callback
= new CryptoOperationHelper.Callback<BenchmarkInputParcel, BenchmarkResult>() {
@@ -662,9 +589,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();
}
@@ -679,10 +604,11 @@ public class KeyListFragment extends LoaderFragment
}
switch (requestCode) {
case REQUEST_DELETE:
case REQUEST_DELETE: {
if (mActionMode != null) {
mActionMode.finish();
}
if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
result.createNotify(getActivity()).show();
@@ -690,8 +616,8 @@ public class KeyListFragment extends LoaderFragment
super.onActivityResult(requestCode, resultCode, data);
}
break;
case REQUEST_ACTION:
}
case REQUEST_ACTION: {
// if a result has been returned, display a notify
if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
@@ -700,8 +626,8 @@ public class KeyListFragment extends LoaderFragment
super.onActivityResult(requestCode, resultCode, data);
}
break;
case REQUEST_VIEW_KEY:
}
case REQUEST_VIEW_KEY: {
if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) {
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
result.createNotify(getActivity()).show();
@@ -709,6 +635,7 @@ public class KeyListFragment extends LoaderFragment
super.onActivityResult(requestCode, resultCode, data);
}
break;
}
}
}
@@ -754,209 +681,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<Integer, Boolean> 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.
* <p/>
* 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();
}
}
}

View File

@@ -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) {

View File

@@ -0,0 +1,587 @@
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 com.futuremind.recyclerviewfastscroll.SectionTitleProvider;
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;
import java.util.Objects;
public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedListAdapter.KeyListCursor, Character,
SectionCursorAdapter.ViewHolder, KeySectionedListAdapter.KeyHeaderViewHolder> implements SectionTitleProvider {
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<Integer> 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 (Integer aSelected : selected) {
notifyItemChanged(aSelected);
}
}
@Override
public String getSectionTitle(int position) {
// this String will be shown in a bubble for specified position
if (moveCursor(getCursorPositionWithoutSections(position))) {
KeyListCursor cursor = getCursor();
if (cursor.isSecret()) {
if (cursor.getKeyId() == 0L) {
mHasDummy = true;
}
return "My";
} else {
String userId = cursor.getRawUserId();
if (TextUtils.isEmpty(userId)) {
return null;
} else {
return userId.substring(0, 1).toUpperCase();
}
}
} else {
Log.w(Constants.TAG, "Unable to determine section title. "
+ "Reason: Could not move cursor over dataset.");
return null;
}
}
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<String> 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);
}
}

View File

@@ -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<C extends CursorAdapter.AbstractCursor, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
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 <em>not</em>
* closed.
*
* @param newCursor The new cursor to be used.
* @return Returns the previously set Cursor, or null if there wasa not one.
* If the given new Cursor is the same instance is the previously set
* Cursor, null is also returned.
*/
public 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;
}
/**
* <p>Converts the cursor into a CharSequence. Subclasses should override this
* method to convert their results. The default implementation returns an
* empty String for null values or the default String representation of
* the value.</p>
*
* @param cursor the cursor to convert to a CharSequence
* @return a CharSequence representing the value
*/
public CharSequence convertToString(Cursor cursor) {
return cursor == null ? "" : cursor.toString();
}
/**
* Called when the {@link ContentObserver} on the cursor receives a change notification.
* The default implementation provides the auto-requery logic, but may be overridden by
* sub classes.
*
* @see ContentObserver#onChange(boolean)
*/
protected void onContentChanged() {
notifyDataSetChanged();
}
private class ChangeObserver extends ContentObserver {
public ChangeObserver() {
super(new Handler());
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
onContentChanged();
}
}
private class MyDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
mDataValid = true;
onContentChanged();
}
@Override
public void onInvalidated() {
mDataValid = false;
onContentChanged();
}
}
public static abstract class AbstractCursor extends CursorWrapper {
public static final String[] PROJECTION = { "_id" };
public static <T extends AbstractCursor> T wrap(Cursor cursor, Class<T> type) {
if (cursor != null) {
try {
Constructor<T> 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<String, Integer> 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<String> 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());
}
}
}

View File

@@ -0,0 +1,318 @@
package org.sufficientlysecure.keychain.ui.util.adapter;
import android.content.Context;
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;
/**
* @param <T> section type.
* @param <VH> the view holder extending {@code BaseViewHolder<Cursor>} that is bound to the cursor data.
* @param <SH> the view holder extending {@code BaseViewHolder<<T>>} that is bound to the section data.
*/
public abstract class SectionCursorAdapter<C extends CursorAdapter.AbstractCursor, T, VH extends SectionCursorAdapter.ViewHolder,
SH extends SectionCursorAdapter.ViewHolder> extends CursorAdapter<C, RecyclerView.ViewHolder> {
public static final String TAG = "SectionCursorAdapter";
private static final short VIEW_TYPE_ITEM = 0x1;
private static final short VIEW_TYPE_SECTION = 0x2;
private SparseArrayCompat<T> mSectionMap = new SparseArrayCompat<>();
private Comparator<T> mSectionComparator;
public SectionCursorAdapter(Context context, C cursor, int flags) {
this(context, cursor, flags, new Comparator<T>() {
@Override
public boolean equal(T obj1, T obj2) {
return (obj1 == null) ?
obj2 == null : obj1.equals(obj2);
}
});
}
public SectionCursorAdapter(Context context, C cursor, int flags, Comparator<T> 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<T> comparator) {
this.mSectionComparator = comparator;
buildSections();
}
/**
* If the adapter's cursor is not null then this method will call buildSections(Cursor cursor).
*/
private void buildSections() {
if (hasValidData()) {
moveCursor(-1);
try {
mSectionMap.clear();
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;
/**
* Return the id of the item represented by the row the cursor
* is currently moved to.
* @param section The section item to get the id from
* @return The id of the dataset
*/
public long getIdFromSection(T section) {
return section != null ? section.hashCode() : 0L;
}
@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 getIdFromSection(section);
}
}
/**
* @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<T> {
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);
}
}
}

View File

@@ -0,0 +1,413 @@
/*
* 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;
/*
* 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.
*/
public class RecyclerFragment<A extends RecyclerView.Adapter> 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);
}
}

View File

@@ -14,6 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sufficientlysecure.keychain.ui.util.recyclerview;
import android.content.Context;

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight" >
<item android:id="@android:id/mask">
<color android:color="@android:color/white" />
</item>
<item>
<selector>
<item android:state_selected="true" android:drawable="@color/pressed_gray"/>
</selector>
</item>
</ripple>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@color/selected_gray"/>
<item android:state_pressed="true" android:drawable="@color/pressed_gray"/>
</selector>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:orientation="horizontal"
android:focusable="true"
android:background="?android:selectableItemBackground">
<LinearLayout
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:paddingRight="4dp"
android:paddingEnd="4dp"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="You don't have any keys yet!"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="Click here to create or import one."
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="16dp"
android:src="@drawable/ic_key_plus_grey600_24dp" />
</LinearLayout>

View File

@@ -1,117 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:fab="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fab="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res-auto"
>
<!--rebuild functionality of ListFragment -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@android:id/progress"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<se.emilsjolander.stickylistheaders.StickyListHeadersListView
android:id="@+id/key_list_list"
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center" />
</LinearLayout>
<RelativeLayout
android:id="@android:id/widget_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<!--rebuild functionality of ListFragment -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawSelectorOnTop="true"
android:fastScrollEnabled="true"
android:paddingLeft="16dp"
android:paddingRight="32dp"
android:scrollbarStyle="outsideOverlay" />
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/key_list_empty"
android:layout_width="match_parent"
android:layout_height="240dp"
android:gravity="center"
android:orientation="vertical"
android:animateLayoutChanges="true"
>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/key_list_empty_text1"
android:textAppearance="?android:attr/textAppearanceLarge" />
<android.support.v7.widget.RecyclerView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="72dp"
android:paddingLeft="16dp"
android:paddingRight="32dp"
android:paddingStart="16dp" />
<org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/search_container"
android:inAnimation="@anim/fade_in_delayed"
android:outAnimation="@anim/fade_out"
android:measureAllChildren="true"
custom:initialView="1">
<Space
<com.futuremind.recyclerviewfastscroll.FastScroller
android:id="@+id/fastscroll"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:orientation="vertical"
app:fastscroll__bubbleColor="@color/primary" />
<Button
</RelativeLayout>
<LinearLayout
android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="240dp"
android:animateLayoutChanges="true"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:id="@+id/search_button"
android:gravity="center"
tools:text="@string/btn_search_for_query"
/>
android:text="@string/key_list_empty_text1"
android:textAppearance="?android:attr/textAppearanceLarge" />
</org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
<org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
android:id="@+id/search_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inAnimation="@anim/fade_in_delayed"
android:measureAllChildren="true"
android:outAnimation="@anim/fade_out"
app:initialView="1">
</LinearLayout>
</FrameLayout>
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.getbase.floatingactionbutton.FloatingActionsMenu
android:id="@+id/fab_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
fab:fab_addButtonColorNormal="?attr/colorPrimary"
fab:fab_addButtonColorPressed="?attr/colorPrimaryDark"
fab:fab_addButtonSize="normal"
fab:fab_addButtonPlusIconColor="@color/icons"
fab:fab_expandDirection="up"
fab:fab_labelStyle="@style/FabMenuStyle"
android:layout_marginBottom="8dp"
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp"
>
<Button
android:id="@+id/search_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="center"
tools:text="@string/btn_search_for_query" />
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_qr_code"
</org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
</LinearLayout>
</FrameLayout>
<com.getbase.floatingactionbutton.FloatingActionsMenu
android:id="@+id/fab_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
fab:fab_icon="@drawable/ic_qrcode_white_24dp"
fab:fab_colorNormal="?attr/colorPrimary"
fab:fab_colorPressed="?attr/colorPrimaryDark"
fab:fab_title="@string/key_list_fab_qr_code"
fab:fab_size="mini" />
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
fab:fab_addButtonColorNormal="?attr/colorPrimary"
fab:fab_addButtonColorPressed="?attr/colorPrimaryDark"
fab:fab_addButtonPlusIconColor="@color/icons"
fab:fab_addButtonSize="normal"
fab:fab_expandDirection="up"
fab:fab_labelStyle="@style/FabMenuStyle">
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_cloud"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
fab:fab_icon="@drawable/ic_cloud_search_24dp"
fab:fab_colorNormal="?attr/colorPrimary"
fab:fab_colorPressed="?attr/colorPrimaryDark"
fab:fab_title="@string/key_list_fab_search"
fab:fab_size="mini" />
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_qr_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
fab:fab_colorNormal="?attr/colorPrimary"
fab:fab_colorPressed="?attr/colorPrimaryDark"
fab:fab_icon="@drawable/ic_qrcode_white_24dp"
fab:fab_size="mini"
fab:fab_title="@string/key_list_fab_qr_code" />
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_cloud"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
fab:fab_colorNormal="?attr/colorPrimary"
fab:fab_colorPressed="?attr/colorPrimaryDark"
fab:fab_icon="@drawable/ic_cloud_search_24dp"
fab:fab_size="mini"
fab:fab_title="@string/key_list_fab_search" />
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
fab:fab_colorNormal="?attr/colorPrimary"
fab:fab_colorPressed="?attr/colorPrimaryDark"
fab:fab_icon="@drawable/ic_folder_white_24dp"
fab:fab_size="mini"
fab:fab_title="@string/key_list_fab_import" />
</com.getbase.floatingactionbutton.FloatingActionsMenu>
</RelativeLayout>
</FrameLayout>
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
fab:fab_icon="@drawable/ic_folder_white_24dp"
fab:fab_colorNormal="?attr/colorPrimary"
fab:fab_colorPressed="?attr/colorPrimaryDark"
fab:fab_title="@string/key_list_fab_import"
fab:fab_size="mini" />
</com.getbase.floatingactionbutton.FloatingActionsMenu>
</RelativeLayout>

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
style="@style/SectionHeader"
android:id="@+id/stickylist_header_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|left"
android:text="header text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="key count"
android:id="@+id/contacts_num"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="8dp"
android:visibility="gone"
android:textColor="@android:color/darker_gray" />
</RelativeLayout>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:super="http://schemas.android.com/apk/lib-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
super:slm_headerDisplay="sticky|inline"
super:slm_section_sectionManager="linear"
tools:ignore="ResAuto">
<TextView style="@style/SectionHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|left"
android:text="@string/my_keys" />
<TextView
android:id="@android:id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="8dp"
android:layout_marginLeft="8dp"
android:textColor="@android:color/darker_gray"
tools:text="11 Keys"/>
</RelativeLayout>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:super="http://schemas.android.com/apk/lib-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
super:slm_headerDisplay="sticky|inline"
super:slm_section_sectionManager="linear"
tools:ignore="ResAuto">
<TextView style="@style/SectionHeader"
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|left"
tools:text="A" />
</FrameLayout>

View File

@@ -1,63 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:minHeight="?attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:singleLine="true"
android:maxLines="1"
android:orientation="horizontal"
android:descendantFocusability="blocksDescendants"
android:background="@drawable/list_item_ripple"
android:focusable="false">
<LinearLayout
android:id="@+id/key_list_item_dummy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:focusable="true"
android:visibility="gone"
android:background="?android:selectableItemBackground"
tools:visibility="visible">
<LinearLayout
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="4dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="You don't have any keys yet!"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="Click here to create or import one."
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="16dp"
android:src="@drawable/ic_key_plus_grey600_24dp"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/key_list_item_data"
android:layout_width="0dip"
@@ -83,7 +38,7 @@
android:id="@+id/key_list_item_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:maxLines="1"
android:ellipsize="end"
tools:text="user@example.com"
android:textAppearance="?android:attr/textAppearanceSmall" />
@@ -92,12 +47,11 @@
android:id="@+id/key_list_item_creation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:visibility="gone"
tools:visibility="visible"
tools:text="Created on 10/10/2010 10:00"
/>
tools:text="Created on 10/10/2010 10:00" />
</LinearLayout>
@@ -122,7 +76,7 @@
android:layout_height="match_parent"
android:layout_gravity="center"
android:src="@drawable/ic_repeat_grey_24dp"
android:padding="12dp"
android:padding="16dp"
android:background="?android:selectableItemBackground"
android:contentDescription="@string/cd_exchange_keys"/>
@@ -134,7 +88,6 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="16dp"
tools:src="@drawable/status_signature_revoked_cutout_24dp"
/>
tools:src="@drawable/status_signature_revoked_cutout_24dp" />
</LinearLayout>

View File

@@ -37,4 +37,7 @@
<color name="card_view_button">#7bad45</color>
<color name="toolbar_photo_tint">#1E7bad45</color>
<color name="pressed_gray">#0c000000</color>
<color name="selected_gray">#2c000000</color>
</resources>