Reintroduce changes made associated with KeyListFragment to new branch.
This commit is contained in:
parent
bf382ec59d
commit
da543345ee
|
@ -33,6 +33,7 @@ dependencies {
|
|||
compile 'org.apache.james:apache-mime4j-dom:0.7.2'
|
||||
compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0'
|
||||
compile 'com.cocosw:bottomsheet:1.3.0@aar'
|
||||
compile 'com.tonicartos:superslim:0.4.13'
|
||||
|
||||
// Material Drawer
|
||||
compile 'com.mikepenz:materialdrawer:5.2.2@aar'
|
||||
|
@ -131,6 +132,7 @@ dependencyVerification {
|
|||
'com.android.support:support-vector-drawable:45b1f180b437a750429f6c1457181c167ba211c17fcb992f83cdbefef5eb1519',
|
||||
'com.squareup.okio:okio:114bdc1f47338a68bcbc95abf2f5cdc72beeec91812f2fcd7b521c1937876266',
|
||||
'com.fidesmo:nordpol-core:3de58e850a00bba5b4d3a604d1399bcd89f695ea191ec0b03a57222e18062d15',
|
||||
'com.tonicartos:superslim:ca89b5c674660cc6918a8f8fd385065bffeee27983e0d33c7c2f0ad7b34d2d49',
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -21,12 +21,10 @@ package org.sufficientlysecure.keychain.ui;
|
|||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MergeCursor;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
@ -45,15 +43,14 @@ import android.view.MenuItem;
|
|||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView.MultiChoiceModeListener;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Button;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.ViewAnimator;
|
||||
|
||||
import com.getbase.floatingactionbutton.FloatingActionButton;
|
||||
import com.getbase.floatingactionbutton.FloatingActionsMenu;
|
||||
import com.tonicartos.superslim.LayoutManager;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
|
||||
|
@ -70,26 +67,25 @@ import org.sufficientlysecure.keychain.service.ConsolidateInputParcel;
|
|||
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeyAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
|
||||
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.ContentDescriptionHint;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.ui.adapter.KeySectionedListAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.util.adapter.CursorAdapter;
|
||||
import org.sufficientlysecure.keychain.ui.util.recyclerview.RecyclerFragment;
|
||||
import org.sufficientlysecure.keychain.util.FabContainer;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
|
||||
import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Public key list with sticky list headers. It does _not_ extend ListFragment because it uses
|
||||
* StickyListHeaders library which does not extend upon ListView.
|
||||
*/
|
||||
public class KeyListFragment extends LoaderFragment
|
||||
implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener,
|
||||
public class KeyListFragment extends RecyclerFragment<KeySectionedListAdapter>
|
||||
implements SearchView.OnQueryTextListener,
|
||||
LoaderManager.LoaderCallbacks<Cursor>, FabContainer,
|
||||
CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> {
|
||||
|
||||
|
@ -97,9 +93,6 @@ public class KeyListFragment extends LoaderFragment
|
|||
private static final int REQUEST_DELETE = 2;
|
||||
private static final int REQUEST_VIEW_KEY = 3;
|
||||
|
||||
private KeyListAdapter mAdapter;
|
||||
private StickyListHeadersListView mStickyList;
|
||||
|
||||
// saves the mode object for multiselect, needed for reset at some point
|
||||
private ActionMode mActionMode = null;
|
||||
|
||||
|
@ -117,16 +110,107 @@ public class KeyListFragment extends LoaderFragment
|
|||
// for ConsolidateOperation
|
||||
private CryptoOperationHelper<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);
|
||||
|
||||
|
@ -157,7 +241,7 @@ public class KeyListFragment extends LoaderFragment
|
|||
});
|
||||
|
||||
|
||||
return root;
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -170,102 +254,13 @@ public class KeyListFragment extends LoaderFragment
|
|||
|
||||
// show app name instead of "keys" from nav drawer
|
||||
final FragmentActivity activity = getActivity();
|
||||
|
||||
activity.setTitle(R.string.app_name);
|
||||
|
||||
mStickyList.setOnItemClickListener(this);
|
||||
mStickyList.setAreHeadersSticky(true);
|
||||
mStickyList.setDrawingListUnderStickyHeader(false);
|
||||
mStickyList.setFastScrollEnabled(true);
|
||||
|
||||
// Adds an empty footer view so that the Floating Action Button won't block content
|
||||
// in last few rows.
|
||||
View footer = new View(activity);
|
||||
|
||||
int spacing = (int) android.util.TypedValue.applyDimension(
|
||||
android.util.TypedValue.COMPLEX_UNIT_DIP, 72, getResources().getDisplayMetrics()
|
||||
);
|
||||
|
||||
android.widget.AbsListView.LayoutParams params = new android.widget.AbsListView.LayoutParams(
|
||||
android.widget.AbsListView.LayoutParams.MATCH_PARENT,
|
||||
spacing
|
||||
);
|
||||
|
||||
footer.setLayoutParams(params);
|
||||
mStickyList.addFooterView(footer, null, false);
|
||||
|
||||
/*
|
||||
* Multi-selection
|
||||
*/
|
||||
mStickyList.setFastScrollAlwaysVisible(true);
|
||||
|
||||
mStickyList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
|
||||
mStickyList.getWrappedList().setMultiChoiceModeListener(new MultiChoiceModeListener() {
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
android.view.MenuInflater inflater = activity.getMenuInflater();
|
||||
inflater.inflate(R.menu.key_list_multi, menu);
|
||||
mActionMode = mode;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
|
||||
// get IDs for checked positions as long array
|
||||
long[] ids;
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_key_list_multi_encrypt: {
|
||||
ids = mAdapter.getCurrentSelectedMasterKeyIds();
|
||||
encrypt(mode, ids);
|
||||
break;
|
||||
}
|
||||
case R.id.menu_key_list_multi_delete: {
|
||||
ids = mAdapter.getCurrentSelectedMasterKeyIds();
|
||||
showDeleteKeyDialog(ids, mAdapter.isAnySecretSelected());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
mActionMode = null;
|
||||
mAdapter.clearSelection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
|
||||
boolean checked) {
|
||||
if (checked) {
|
||||
mAdapter.setNewSelection(position, true);
|
||||
} else {
|
||||
mAdapter.removeSelection(position);
|
||||
}
|
||||
int count = mStickyList.getCheckedItemCount();
|
||||
String keysSelected = getResources().getQuantityString(
|
||||
R.plurals.key_list_selected_keys, count, count);
|
||||
mode.setTitle(keysSelected);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// We have a menu item to show in action bar.
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
// Start out with a progress indicator.
|
||||
setContentShown(false);
|
||||
|
||||
// this view is made visible if no data is available
|
||||
mStickyList.setEmptyView(activity.findViewById(R.id.key_list_empty));
|
||||
hideList(false);
|
||||
|
||||
// click on search button (in empty view) starts query for search string
|
||||
vSearchContainer = (ViewAnimator) activity.findViewById(R.id.search_container);
|
||||
|
@ -278,8 +273,13 @@ public class KeyListFragment extends LoaderFragment
|
|||
});
|
||||
|
||||
// Create an empty adapter we will use to display the loaded data.
|
||||
mAdapter = new KeyListAdapter(activity, null, 0);
|
||||
mStickyList.setAdapter(mAdapter);
|
||||
//mAdapter = new KeyListAdapter(activity, null, 0);
|
||||
|
||||
KeySectionedListAdapter adapter = new KeySectionedListAdapter(getContext(), null);
|
||||
adapter.setKeyListener(mKeyListener);
|
||||
|
||||
setAdapter(adapter);
|
||||
setLayoutManager(new LayoutManager(getActivity()));
|
||||
|
||||
// Prepare the loader. Either re-connect with an existing one,
|
||||
// or start a new one.
|
||||
|
@ -298,9 +298,6 @@ public class KeyListFragment extends LoaderFragment
|
|||
startActivity(searchIntent);
|
||||
}
|
||||
|
||||
static final String ORDER =
|
||||
KeyRings.HAS_ANY_SECRET + " DESC, " + KeyRings.USER_ID + " COLLATE NOCASE ASC";
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
// This is called when a new Loader needs to be created. This
|
||||
|
@ -314,31 +311,17 @@ public class KeyListFragment extends LoaderFragment
|
|||
|
||||
// Now create and return a CursorLoader that will take care of
|
||||
// creating a Cursor for the data being displayed.
|
||||
return new CursorLoader(getActivity(), uri, KeyListAdapter.PROJECTION, null, null, ORDER);
|
||||
return new CursorLoader(getActivity(), uri,
|
||||
KeySectionedListAdapter.KeyListCursor.PROJECTION, null, null,
|
||||
KeySectionedListAdapter.KeyListCursor.ORDER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<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) {
|
||||
|
@ -347,9 +330,9 @@ public class KeyListFragment extends LoaderFragment
|
|||
|
||||
// The list should now be shown.
|
||||
if (isResumed()) {
|
||||
setContentShown(true);
|
||||
showList(true);
|
||||
} else {
|
||||
setContentShownNoAnimation(true);
|
||||
showList(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -358,47 +341,9 @@ public class KeyListFragment extends LoaderFragment
|
|||
// This is called when the last Cursor provided to onLoadFinished()
|
||||
// above is about to be closed. We need to make sure we are no
|
||||
// longer using it.
|
||||
mAdapter.swapCursor(null);
|
||||
getAdapter().swapCursor(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* On click on item, start key view activity
|
||||
*/
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
|
||||
Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class);
|
||||
viewIntent.setData(
|
||||
KeyRings.buildGenericKeyRingUri(mAdapter.getMasterKeyId(position)));
|
||||
startActivityForResult(viewIntent, REQUEST_VIEW_KEY);
|
||||
}
|
||||
|
||||
protected void encrypt(ActionMode mode, long[] masterKeyIds) {
|
||||
Intent intent = new Intent(getActivity(), EncryptFilesActivity.class);
|
||||
intent.setAction(EncryptFilesActivity.ACTION_ENCRYPT_DATA);
|
||||
intent.putExtra(EncryptFilesActivity.EXTRA_ENCRYPTION_KEY_IDS, masterKeyIds);
|
||||
// used instead of startActivity set actionbar based on callingPackage
|
||||
startActivityForResult(intent, REQUEST_ACTION);
|
||||
|
||||
mode.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show dialog to delete key
|
||||
*
|
||||
* @param hasSecret must contain whether the list of masterKeyIds contains a secret key or not
|
||||
*/
|
||||
public void showDeleteKeyDialog(long[] masterKeyIds, boolean hasSecret) {
|
||||
Intent intent = new Intent(getActivity(), DeleteKeyDialogActivity.class);
|
||||
intent.putExtra(DeleteKeyDialogActivity.EXTRA_DELETE_MASTER_KEY_IDS, masterKeyIds);
|
||||
intent.putExtra(DeleteKeyDialogActivity.EXTRA_HAS_SECRET, hasSecret);
|
||||
if (hasSecret) {
|
||||
intent.putExtra(DeleteKeyDialogActivity.EXTRA_KEYSERVER,
|
||||
Preferences.getPreferences(getActivity()).getPreferredKeyserver());
|
||||
}
|
||||
startActivityForResult(intent, REQUEST_DELETE);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.key_list, menu);
|
||||
|
@ -413,7 +358,6 @@ public class KeyListFragment extends LoaderFragment
|
|||
|
||||
// Get the searchview
|
||||
MenuItem searchItem = menu.findItem(R.id.menu_key_list_search);
|
||||
|
||||
SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
|
||||
|
||||
// Execute this when searching
|
||||
|
@ -558,7 +502,6 @@ public class KeyListFragment extends LoaderFragment
|
|||
}
|
||||
|
||||
ProviderHelper providerHelper = new ProviderHelper(activity);
|
||||
|
||||
Cursor cursor = providerHelper.getContentResolver().query(
|
||||
KeyRings.buildUnifiedKeyRingsUri(), new String[]{
|
||||
KeyRings.FINGERPRINT
|
||||
|
@ -573,7 +516,7 @@ public class KeyListFragment extends LoaderFragment
|
|||
ArrayList<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);
|
||||
keyList.add(keyEntry);
|
||||
|
@ -592,7 +535,6 @@ public class KeyListFragment extends LoaderFragment
|
|||
}
|
||||
|
||||
private void consolidate() {
|
||||
|
||||
CryptoOperationHelper.Callback<ConsolidateInputParcel, ConsolidateResult> callback
|
||||
= new CryptoOperationHelper.Callback<ConsolidateInputParcel, ConsolidateResult>() {
|
||||
|
||||
|
@ -622,14 +564,11 @@ public class KeyListFragment extends LoaderFragment
|
|||
}
|
||||
};
|
||||
|
||||
mConsolidateOpHelper =
|
||||
new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing);
|
||||
|
||||
mConsolidateOpHelper = new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing);
|
||||
mConsolidateOpHelper.cryptoOperation();
|
||||
}
|
||||
|
||||
private void benchmark() {
|
||||
|
||||
CryptoOperationHelper.Callback<BenchmarkInputParcel, BenchmarkResult> callback
|
||||
= new CryptoOperationHelper.Callback<BenchmarkInputParcel, BenchmarkResult>() {
|
||||
|
||||
|
@ -659,9 +598,7 @@ public class KeyListFragment extends LoaderFragment
|
|||
}
|
||||
};
|
||||
|
||||
CryptoOperationHelper opHelper =
|
||||
new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing);
|
||||
|
||||
CryptoOperationHelper opHelper = new CryptoOperationHelper<>(2, this, callback, R.string.progress_importing);
|
||||
opHelper.cryptoOperation();
|
||||
}
|
||||
|
||||
|
@ -680,6 +617,7 @@ public class KeyListFragment extends LoaderFragment
|
|||
if (mActionMode != null) {
|
||||
mActionMode.finish();
|
||||
}
|
||||
|
||||
if (data != null && data.hasExtra(OperationResult.EXTRA_RESULT)) {
|
||||
OperationResult result = data.getParcelableExtra(OperationResult.EXTRA_RESULT);
|
||||
result.createNotify(getActivity()).show();
|
||||
|
@ -751,209 +689,4 @@ public class KeyListFragment extends LoaderFragment
|
|||
public boolean onCryptoSetProgress(String msg, int progress, int max) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public class KeyListAdapter extends KeyAdapter implements StickyListHeadersAdapter {
|
||||
|
||||
private HashMap<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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,553 @@
|
|||
package org.sufficientlysecure.keychain.ui.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MergeCursor;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.Highlighter;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.ui.util.adapter.*;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class KeySectionedListAdapter extends SectionCursorAdapter<KeySectionedListAdapter.KeyListCursor, Character,
|
||||
SectionCursorAdapter.ViewHolder, KeySectionedListAdapter.KeyHeaderViewHolder> {
|
||||
|
||||
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(int i = 0; i < selected.length; i++) {
|
||||
notifyItemChanged(selected[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private class KeyDummyViewHolder extends SectionCursorAdapter.ViewHolder
|
||||
implements View.OnClickListener{
|
||||
|
||||
KeyDummyViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
itemView.setClickable(true);
|
||||
itemView.setOnClickListener(this);
|
||||
itemView.setEnabled(getSelectedCount() == 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if(mListener != null) {
|
||||
mListener.onKeyDummyItemClicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class KeyItemViewHolder extends SectionCursorAdapter.ViewHolder
|
||||
implements View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
private TextView mMainUserId;
|
||||
private TextView mMainUserIdRest;
|
||||
private TextView mCreationDate;
|
||||
private ImageView mStatus;
|
||||
private View mSlinger;
|
||||
private ImageButton mSlingerButton;
|
||||
|
||||
KeyItemViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
mMainUserId = (TextView) itemView.findViewById(R.id.key_list_item_name);
|
||||
mMainUserIdRest = (TextView) itemView.findViewById(R.id.key_list_item_email);
|
||||
mStatus = (ImageView) itemView.findViewById(R.id.key_list_item_status_icon);
|
||||
mSlinger = itemView.findViewById(R.id.key_list_item_slinger_view);
|
||||
mSlingerButton = (ImageButton) itemView.findViewById(R.id.key_list_item_slinger_button);
|
||||
mCreationDate = (TextView) itemView.findViewById(R.id.key_list_item_creation);
|
||||
|
||||
itemView.setClickable(true);
|
||||
itemView.setLongClickable(true);
|
||||
itemView.setOnClickListener(this);
|
||||
itemView.setOnLongClickListener(this);
|
||||
|
||||
mSlingerButton.setClickable(true);
|
||||
mSlingerButton.setOnClickListener(this);
|
||||
}
|
||||
|
||||
void bindKey(KeyListCursor keyItem, Highlighter highlighter) {
|
||||
itemView.setSelected(isSelected(getAdapterPosition()));
|
||||
Context context = itemView.getContext();
|
||||
|
||||
{ // set name and stuff, common to both key types
|
||||
OpenPgpUtils.UserId userIdSplit = keyItem.getUserId();
|
||||
if (userIdSplit.name != null) {
|
||||
mMainUserId.setText(highlighter.highlight(userIdSplit.name));
|
||||
} else {
|
||||
mMainUserId.setText(R.string.user_id_no_name);
|
||||
}
|
||||
if (userIdSplit.email != null) {
|
||||
mMainUserIdRest.setText(highlighter.highlight(userIdSplit.email));
|
||||
mMainUserIdRest.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mMainUserIdRest.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
{ // set edit button and status, specific by key type. Note: order is important!
|
||||
int textColor;
|
||||
if (keyItem.isRevoked()) {
|
||||
KeyFormattingUtils.setStatusImage(
|
||||
context,
|
||||
mStatus,
|
||||
null,
|
||||
KeyFormattingUtils.State.REVOKED,
|
||||
R.color.key_flag_gray
|
||||
);
|
||||
|
||||
mStatus.setVisibility(View.VISIBLE);
|
||||
mSlinger.setVisibility(View.GONE);
|
||||
textColor = ContextCompat.getColor(context, R.color.key_flag_gray);
|
||||
} else if (keyItem.isExpired()) {
|
||||
KeyFormattingUtils.setStatusImage(
|
||||
context,
|
||||
mStatus,
|
||||
null,
|
||||
KeyFormattingUtils.State.EXPIRED,
|
||||
R.color.key_flag_gray
|
||||
);
|
||||
|
||||
mStatus.setVisibility(View.VISIBLE);
|
||||
mSlinger.setVisibility(View.GONE);
|
||||
textColor = ContextCompat.getColor(context, R.color.key_flag_gray);
|
||||
} else if (keyItem.isSecret()) {
|
||||
mStatus.setVisibility(View.GONE);
|
||||
if (mSlingerButton.hasOnClickListeners()) {
|
||||
mSlingerButton.setColorFilter(
|
||||
FormattingUtils.getColorFromAttr(context, R.attr.colorTertiaryText),
|
||||
PorterDuff.Mode.SRC_IN
|
||||
);
|
||||
|
||||
mSlinger.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mSlinger.setVisibility(View.GONE);
|
||||
}
|
||||
textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText);
|
||||
} else {
|
||||
// this is a public key - show if it's verified
|
||||
if (keyItem.isVerified()) {
|
||||
KeyFormattingUtils.setStatusImage(
|
||||
context,
|
||||
mStatus,
|
||||
KeyFormattingUtils.State.VERIFIED
|
||||
);
|
||||
|
||||
mStatus.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
KeyFormattingUtils.setStatusImage(
|
||||
context,
|
||||
mStatus,
|
||||
KeyFormattingUtils.State.UNVERIFIED
|
||||
);
|
||||
|
||||
mStatus.setVisibility(View.VISIBLE);
|
||||
}
|
||||
mSlinger.setVisibility(View.GONE);
|
||||
textColor = FormattingUtils.getColorFromAttr(context, R.attr.colorText);
|
||||
}
|
||||
|
||||
mMainUserId.setTextColor(textColor);
|
||||
mMainUserIdRest.setTextColor(textColor);
|
||||
|
||||
if (keyItem.hasDuplicate()) {
|
||||
String dateTime = DateUtils.formatDateTime(context,
|
||||
keyItem.getCreationTime(),
|
||||
DateUtils.FORMAT_SHOW_DATE
|
||||
| DateUtils.FORMAT_SHOW_TIME
|
||||
| DateUtils.FORMAT_SHOW_YEAR
|
||||
| DateUtils.FORMAT_ABBREV_MONTH);
|
||||
mCreationDate.setText(context.getString(R.string.label_key_created,
|
||||
dateTime));
|
||||
mCreationDate.setTextColor(textColor);
|
||||
mCreationDate.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mCreationDate.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int pos = getAdapterPosition();
|
||||
switch (v.getId()) {
|
||||
case R.id.key_list_item_slinger_button:
|
||||
if (mListener != null) {
|
||||
mListener.onSlingerButtonClicked(getItemId());
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (getSelectedCount() == 0) {
|
||||
if (mListener != null) {
|
||||
mListener.onKeyItemClicked(getItemId());
|
||||
}
|
||||
} else {
|
||||
if (isSelected(pos)) {
|
||||
deselectPosition(pos);
|
||||
} else {
|
||||
selectPosition(pos);
|
||||
}
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onSelectionStateChanged(getSelectedCount());
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
System.out.println("Long Click!");
|
||||
if (getSelectedCount() == 0) {
|
||||
selectPosition(getAdapterPosition());
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onSelectionStateChanged(getSelectedCount());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static class KeyHeaderViewHolder extends SectionCursorAdapter.ViewHolder {
|
||||
private TextView mText1;
|
||||
|
||||
public KeyHeaderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
mText1 = (TextView) itemView.findViewById(android.R.id.text1);
|
||||
}
|
||||
|
||||
public void bind(String title) {
|
||||
mText1.setText(title);
|
||||
}
|
||||
}
|
||||
|
||||
public static class KeyListCursor extends CursorAdapter.KeyCursor {
|
||||
public static final String ORDER = KeychainContract.KeyRings.HAS_ANY_SECRET
|
||||
+ " DESC, " + KeychainContract.KeyRings.USER_ID + " COLLATE NOCASE ASC";
|
||||
|
||||
public static final String[] PROJECTION;
|
||||
|
||||
static {
|
||||
ArrayList<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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,318 @@
|
|||
package org.sufficientlysecure.keychain.ui.util.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.util.SparseArrayCompat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.tonicartos.superslim.LayoutManager;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @param <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;
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return super.getItemCount() + mSectionMap.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final long getItemId(int listPosition) {
|
||||
/*
|
||||
int index = mSectionMap.indexOfKey(listPosition);
|
||||
if (index < 0) {
|
||||
int cursorPosition = getCursorPositionWithoutSections(listPosition);
|
||||
return super.getItemId(cursorPosition);
|
||||
} else {
|
||||
T section = mSectionMap.valueAt(index);
|
||||
return section != null ? section.hashCode() : 0L;
|
||||
} */
|
||||
|
||||
if (isSection(listPosition)) {
|
||||
return RecyclerView.NO_ID;
|
||||
} else {
|
||||
int cursorPosition = getCursorPositionWithoutSections(listPosition);
|
||||
return super.getItemId(cursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param listPosition the position of the current item in the list with mSectionMap included
|
||||
* @return Whether or not the listPosition points to a section.
|
||||
*/
|
||||
public boolean isSection(int listPosition) {
|
||||
return mSectionMap.indexOfKey(listPosition) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will map a position in the list adapter (which includes mSectionMap) to a position in
|
||||
* the cursor (which does not contain mSectionMap).
|
||||
*
|
||||
* @param listPosition the position of the current item in the list with mSectionMap included
|
||||
* @return the correct position to use with the cursor
|
||||
*/
|
||||
public int getCursorPositionWithoutSections(int listPosition) {
|
||||
if (mSectionMap.size() == 0) {
|
||||
return listPosition;
|
||||
} else if (!isSection(listPosition)) {
|
||||
int sectionIndex = getSectionForPosition(listPosition);
|
||||
if (isListPositionBeforeFirstSection(listPosition, sectionIndex)) {
|
||||
return listPosition;
|
||||
} else {
|
||||
return listPosition - (sectionIndex + 1);
|
||||
}
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public int getListPosition(int cursorPosition) {
|
||||
for(int i = 0; i < mSectionMap.size(); i++) {
|
||||
int sectionIndex = mSectionMap.keyAt(i);
|
||||
if (sectionIndex > cursorPosition) {
|
||||
return cursorPosition;
|
||||
}
|
||||
|
||||
cursorPosition +=1;
|
||||
}
|
||||
|
||||
return cursorPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the list position of an item in the adapter, returns the
|
||||
* adapter position of the first item of the section the given item belongs to.
|
||||
* @param listPosition The absolute list position.
|
||||
* @return The position of the first item of the section.
|
||||
*/
|
||||
public int getFirstSectionPosition(int listPosition) {
|
||||
int start = 0;
|
||||
for(int i = 0; i <= listPosition; i++) {
|
||||
if(isSection(i)) {
|
||||
start = i;
|
||||
}
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
|
||||
public int getSectionForPosition(int listPosition) {
|
||||
boolean isSection = false;
|
||||
int numPrecedingSections = 0;
|
||||
for (int i = 0; i < mSectionMap.size(); i++) {
|
||||
int sectionPosition = mSectionMap.keyAt(i);
|
||||
|
||||
if (listPosition > sectionPosition) {
|
||||
numPrecedingSections++;
|
||||
} else if (listPosition == sectionPosition) {
|
||||
isSection = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isSection ? numPrecedingSections : Math.max(numPrecedingSections - 1, 0);
|
||||
}
|
||||
|
||||
private boolean isListPositionBeforeFirstSection(int listPosition, int sectionIndex) {
|
||||
boolean hasSections = mSectionMap != null && mSectionMap.size() > 0;
|
||||
return sectionIndex == 0 && hasSections && listPosition < mSectionMap.keyAt(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getItemViewType(int listPosition) {
|
||||
int sectionIndex = mSectionMap.indexOfKey(listPosition);
|
||||
if(sectionIndex < 0) {
|
||||
int cursorPosition = getCursorPositionWithoutSections(listPosition);
|
||||
return (getSectionItemViewType(cursorPosition) << 16) | VIEW_TYPE_ITEM;
|
||||
} else {
|
||||
return (getSectionHeaderViewType(sectionIndex) << 16) | VIEW_TYPE_SECTION;
|
||||
}
|
||||
}
|
||||
|
||||
protected short getSectionHeaderViewType(int sectionIndex) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected short getSectionItemViewType(int position) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
LayoutManager.LayoutParams layoutParams = LayoutManager.LayoutParams
|
||||
.from(holder.itemView.getLayoutParams());
|
||||
|
||||
// assign first position of section to each item
|
||||
layoutParams.setFirstPosition(getFirstSectionPosition(position));
|
||||
|
||||
int viewType = holder.getItemViewType() & 0xFF;
|
||||
switch (viewType) {
|
||||
case VIEW_TYPE_ITEM :
|
||||
moveCursorOrThrow(getCursorPositionWithoutSections(position));
|
||||
onBindItemViewHolder((VH) holder, getCursor());
|
||||
|
||||
layoutParams.isHeader = false;
|
||||
break;
|
||||
|
||||
case VIEW_TYPE_SECTION:
|
||||
T section = mSectionMap.get(position);
|
||||
onBindSectionViewHolder((SH) holder, section);
|
||||
|
||||
layoutParams.isHeader = true;
|
||||
break;
|
||||
}
|
||||
|
||||
holder.itemView.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
switch (viewType & 0xFF) {
|
||||
case VIEW_TYPE_SECTION:
|
||||
return onCreateSectionViewHolder(parent, viewType >> 16);
|
||||
|
||||
case VIEW_TYPE_ITEM:
|
||||
return onCreateItemViewHolder(parent, viewType >> 16);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract SH onCreateSectionViewHolder(ViewGroup parent, int viewType);
|
||||
protected abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType);
|
||||
|
||||
protected abstract void onBindSectionViewHolder(SH holder, T section);
|
||||
protected abstract void onBindItemViewHolder(VH holder, C cursor);
|
||||
|
||||
public interface Comparator<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,414 @@
|
|||
/*
|
||||
* Implementation of taken from the sourcecode of
|
||||
* android.support.v4.app.ListFragment from the
|
||||
* Android Open Source Project and changed to use
|
||||
* RecyclerView instead of ListView.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui.util.recyclerview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
public class RecyclerFragment<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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package org.sufficientlysecure.keychain.ui.util.recyclerview.fastscroll;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.StaggeredGridLayoutManager;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import com.tonicartos.superslim.LayoutManager;
|
||||
|
||||
public class FastScrollRecyclerView extends RecyclerView implements RecyclerView.OnItemTouchListener {
|
||||
private FastScroller mFastScroller;
|
||||
|
||||
private int mLastX;
|
||||
private int mLastY;
|
||||
|
||||
public FastScrollRecyclerView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public FastScrollRecyclerView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public FastScrollRecyclerView(Context context, AttributeSet attributeSet, int defStyleAttr) {
|
||||
super(context, attributeSet, defStyleAttr);
|
||||
mFastScroller = new FastScroller(this, attributeSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
addOnItemTouchListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
mFastScroller.draw(canvas);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(int x, int y) {
|
||||
super.onScrolled(x, y);
|
||||
mFastScroller.updateThumb(
|
||||
computeVerticalScrollOffset(),
|
||||
computeVerticalScrollExtent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
mFastScroller.updateContainer(top, right, bottom);
|
||||
mFastScroller.updateThumb(
|
||||
computeVerticalScrollOffset(),
|
||||
computeVerticalScrollExtent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(int state) {
|
||||
switch (state) {
|
||||
case SCROLL_STATE_IDLE:
|
||||
mFastScroller.hideBar();
|
||||
break;
|
||||
case SCROLL_STATE_DRAGGING:
|
||||
mFastScroller.showBar();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
|
||||
if(mFastScroller.onInterceptTouchEvent(e)) {
|
||||
onTouchEvent(rv, e);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEvent(RecyclerView rv, MotionEvent event) {
|
||||
int x = (int) event.getX();
|
||||
int y = (int) event.getY();
|
||||
|
||||
mFastScroller.handleTouchEvent(event.getAction(), x, y, mLastX, mLastY);
|
||||
mLastX = x;
|
||||
mLastY = y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
|
||||
|
||||
}
|
||||
|
||||
private int getItemCount() {
|
||||
return getAdapter() != null ?
|
||||
getAdapter().getItemCount() : 0;
|
||||
}
|
||||
|
||||
public void scrollToFraction(float fraction) {
|
||||
int count = getItemCount();
|
||||
if (count > 0) {
|
||||
stopScroll();
|
||||
scrollToPosition((int) ((count - 1) * fraction));
|
||||
}
|
||||
}
|
||||
|
||||
public void scrollByFraction(float fraction) {
|
||||
int count = getItemCount();
|
||||
if (count > 0) {
|
||||
stopScroll();
|
||||
|
||||
|
||||
int pixelsToScroll = (int) (computeVerticalScrollRange() * fraction);
|
||||
System.out.println("ScrollBy Fraction: " + fraction + ", Pixel: " + pixelsToScroll);
|
||||
|
||||
scrollBy(0, pixelsToScroll);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
package org.sufficientlysecure.keychain.ui.util.recyclerview.fastscroll;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.support.v4.view.animation.FastOutLinearInInterpolator;
|
||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
|
||||
|
||||
public class FastScroller {
|
||||
private static final int DEFAULT_AUTO_HIDE_DELAY = 1500;
|
||||
private static final boolean DEFAULT_AUTO_HIDE_ENABLED = true;
|
||||
|
||||
private Paint mTrack;
|
||||
private Paint mThumb;
|
||||
|
||||
private int mPosX;
|
||||
private int mPosY;
|
||||
private int mWidth;
|
||||
private int mHeight;
|
||||
|
||||
private Rect mContainer;
|
||||
private boolean mIsDragging;
|
||||
|
||||
private int mAutoHideDelay;
|
||||
private boolean mAutoHideEnabled;
|
||||
|
||||
private final int mTrackColorNormal;
|
||||
private final int mTrackColorDragging;
|
||||
|
||||
private final int mThumbColorNormal;
|
||||
private final int mThumbColorDragging;
|
||||
|
||||
private ValueAnimator mBarAnimator;
|
||||
private final FastScrollRecyclerView mRecyclerView;
|
||||
|
||||
private final int mTouchSlop;
|
||||
private final int mTouchInset;
|
||||
private final int[] mKeyFrames;
|
||||
|
||||
//private final Runnable mHideRunnable;
|
||||
|
||||
public FastScroller(final FastScrollRecyclerView recyclerView, AttributeSet attributeSet) {
|
||||
Context context = recyclerView.getContext();
|
||||
mRecyclerView = recyclerView;
|
||||
|
||||
mTrack = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mThumb = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
mKeyFrames = new int[2];
|
||||
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
||||
mTouchInset = FormattingUtils.dpToPx(context, 8);
|
||||
|
||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(
|
||||
attributeSet, R.styleable.FastScroller, 0, 0);
|
||||
|
||||
try {
|
||||
mWidth = typedArray.getDimensionPixelSize(
|
||||
R.styleable.FastScroller_fastScrollScrollBarWidth, FormattingUtils.dpToPx(context, 6));
|
||||
|
||||
mAutoHideDelay = typedArray.getInteger(
|
||||
R.styleable.FastScroller_fastScrollAutoHideDelay, DEFAULT_AUTO_HIDE_DELAY);
|
||||
mAutoHideEnabled = typedArray.getBoolean(
|
||||
R.styleable.FastScroller_fastScrollAutoHideEnabled, DEFAULT_AUTO_HIDE_ENABLED);
|
||||
|
||||
mTrackColorNormal = typedArray.getColor(
|
||||
R.styleable.FastScroller_fastScrollTrackColorNormal, Color.LTGRAY);
|
||||
mTrackColorDragging = typedArray.getColor(
|
||||
R.styleable.FastScroller_fastScrollTrackColorDragging, Color.GRAY);
|
||||
|
||||
mThumbColorNormal = typedArray.getColor(
|
||||
R.styleable.FastScroller_fastScrollThumbColorNormal, Color.GRAY);
|
||||
mThumbColorDragging = typedArray.getColor(
|
||||
R.styleable.FastScroller_fastScrollThumbColorDragging, Color.BLUE);
|
||||
|
||||
mTrack.setColor(mTrackColorNormal);
|
||||
mThumb.setColor(mThumbColorNormal);
|
||||
} finally {
|
||||
typedArray.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public void hideBar() {
|
||||
if(mPosX >= mWidth || !mAutoHideEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
mKeyFrames[0] = mPosX;
|
||||
mKeyFrames[1] = mWidth;
|
||||
|
||||
prepareAnimator();
|
||||
mBarAnimator.setIntValues(mKeyFrames);
|
||||
mBarAnimator.setStartDelay(mAutoHideDelay);
|
||||
mBarAnimator.setInterpolator(new FastOutLinearInInterpolator());
|
||||
mBarAnimator.start();
|
||||
}
|
||||
|
||||
public void showBar() {
|
||||
if(mPosX < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
mKeyFrames[0] = mPosX;
|
||||
mKeyFrames[1] = 0;
|
||||
|
||||
prepareAnimator();
|
||||
mBarAnimator.setStartDelay(0);
|
||||
mBarAnimator.setIntValues(mKeyFrames);
|
||||
mBarAnimator.setInterpolator(new FastOutSlowInInterpolator());
|
||||
mBarAnimator.start();
|
||||
}
|
||||
|
||||
public boolean isAnimatingShow() {
|
||||
return mKeyFrames[1] == 0 && mBarAnimator.isStarted();
|
||||
}
|
||||
|
||||
public boolean isAnimatingHide() {
|
||||
return mKeyFrames[1] == mWidth && mBarAnimator.isStarted();
|
||||
}
|
||||
|
||||
public boolean isBarFullyShown() {
|
||||
return mPosX < 1;
|
||||
}
|
||||
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
return mContainer != null
|
||||
&& (event.getX() < mContainer.right)
|
||||
&& (event.getX() > mContainer.left - mTouchInset)
|
||||
&& (event.getY() < mContainer.bottom)
|
||||
&& (event.getY() > mContainer.top);
|
||||
}
|
||||
|
||||
private boolean isInThumb(int x, int y) {
|
||||
return x > (mContainer.left + mPosX - mTouchInset)
|
||||
&& x < (mContainer.right + mTouchInset)
|
||||
&& y > (mContainer.top + mPosY - mTouchInset)
|
||||
&& y < (mContainer.top + mPosY + mHeight + mTouchInset);
|
||||
}
|
||||
|
||||
public void draw(Canvas canvas) {
|
||||
if(mPosX < 0 || mPosX >= mWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!mRecyclerView.canScrollVertically(-1)
|
||||
&& ! mRecyclerView.canScrollVertically(1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int topBound = mContainer.top + mPosY;
|
||||
int leftBound = mContainer.left + mPosX;
|
||||
|
||||
canvas.drawRect(leftBound, mContainer.top, mContainer.right, mContainer.bottom, mTrack);
|
||||
canvas.drawRect(leftBound, topBound, mContainer.right, topBound + mHeight, mThumb);
|
||||
}
|
||||
|
||||
public void updateThumb(int offset, int extent) {
|
||||
mPosY = offset;
|
||||
mHeight = extent;
|
||||
}
|
||||
|
||||
public void updateContainer(int top, int right, int bottom) {
|
||||
mPosX = 0;
|
||||
mContainer = new Rect(right - mWidth, top, right, bottom);
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void invalidate() {
|
||||
mRecyclerView.invalidate(mContainer);
|
||||
}
|
||||
|
||||
private void prepareAnimator() {
|
||||
if (mBarAnimator != null) {
|
||||
mBarAnimator.cancel();
|
||||
} else {
|
||||
mBarAnimator = new ValueAnimator();
|
||||
mBarAnimator.setDuration(150);
|
||||
mBarAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
mPosX = (Integer) animation.getAnimatedValue();
|
||||
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void handleTouchEvent(int action, int x, int y, int lastX, int lastY) {
|
||||
if(!mRecyclerView.canScrollVertically(-1)
|
||||
&& ! mRecyclerView.canScrollVertically(1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
if(isBarFullyShown()) { //
|
||||
prepareAnimator(); // cancel any pending animations
|
||||
|
||||
mTrack.setColor(mTrackColorDragging);
|
||||
mThumb.setColor(mThumbColorDragging);
|
||||
|
||||
if(!isInThumb(x, y)) {
|
||||
// jump to point
|
||||
mPosY = Math.min(
|
||||
Math.max(mContainer.top, y - (mHeight / 2)),
|
||||
mContainer.bottom - mHeight
|
||||
);
|
||||
|
||||
float range = (mContainer.bottom - mContainer.top) - mHeight;
|
||||
mRecyclerView.scrollToFraction(mPosY / range);
|
||||
} else {
|
||||
invalidate();
|
||||
}
|
||||
} else {
|
||||
if(!isAnimatingShow()) {
|
||||
showBar();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if((!mIsDragging
|
||||
&& isInThumb(x, y)
|
||||
&& Math.abs(y - lastY) > mTouchSlop)
|
||||
|| mIsDragging) {
|
||||
|
||||
if(!mIsDragging) {
|
||||
mIsDragging = true;
|
||||
}
|
||||
|
||||
float dist = y - lastY;
|
||||
float range = (mContainer.bottom - mContainer.top) - mHeight;
|
||||
|
||||
if(mRecyclerView.canScrollVertically(dist < 0 ? -1 : 1)) {
|
||||
mRecyclerView.scrollByFraction(dist / range);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if(mIsDragging) {
|
||||
mIsDragging = false;
|
||||
}
|
||||
|
||||
mTrack.setColor(mTrackColorNormal);
|
||||
mThumb.setColor(mThumbColorNormal);
|
||||
|
||||
if(!mBarAnimator.isRunning()
|
||||
&& mRecyclerView.getScrollState()
|
||||
== RecyclerView.SCROLL_STATE_IDLE) {
|
||||
hideBar();
|
||||
invalidate();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
12
OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml
Normal file
12
OpenKeychain/src/main/res/drawable-v21/list_item_ripple.xml
Normal 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>
|
5
OpenKeychain/src/main/res/drawable/list_item_ripple.xml
Normal file
5
OpenKeychain/src/main/res/drawable/list_item_ripple.xml
Normal 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>
|
47
OpenKeychain/src/main/res/layout/key_list_dummy.xml
Normal file
47
OpenKeychain/src/main/res/layout/key_list_dummy.xml
Normal 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>
|
|
@ -1,117 +1,144 @@
|
|||
<?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:fab="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:custom="http://schemas.android.com/apk/res-auto"
|
||||
>
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
|
||||
<!--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:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center">
|
||||
|
||||
<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_height="match_parent"
|
||||
android:layout_width="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"
|
||||
>
|
||||
<org.sufficientlysecure.keychain.ui.util.recyclerview.fastscroll.FastScrollRecyclerView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingRight="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:paddingBottom="72dp"
|
||||
android:clipToPadding="false"
|
||||
custom:fastScrollScrollBarWidth="8dp"
|
||||
custom:fastScrollThumbColorNormal="@color/selected_gray"
|
||||
custom:fastScrollThumbColorDragging="@color/accent" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
<LinearLayout
|
||||
android:id="@android:id/empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="240dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/key_list_empty_text1"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge" />
|
||||
android:orientation="vertical"
|
||||
android:animateLayoutChanges="true" >
|
||||
|
||||
<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
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
<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: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">
|
||||
|
||||
</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: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"/>
|
||||
|
||||
<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_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">
|
||||
|
||||
<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_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" />
|
||||
|
||||
<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_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>
|
||||
|
||||
</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>
|
||||
|
|
|
@ -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>
|
34
OpenKeychain/src/main/res/layout/key_list_header_private.xml
Normal file
34
OpenKeychain/src/main/res/layout/key_list_header_private.xml
Normal 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>
|
22
OpenKeychain/src/main/res/layout/key_list_header_public.xml
Normal file
22
OpenKeychain/src/main/res/layout/key_list_header_public.xml
Normal 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>
|
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
|
|
@ -28,4 +28,14 @@
|
|||
<attr name="prefix" format="string" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="FastScroller">
|
||||
<attr name="fastScrollScrollBarWidth" format="dimension"/>
|
||||
<attr name="fastScrollAutoHideEnabled" format="boolean" />
|
||||
<attr name="fastScrollAutoHideDelay" format="integer" />
|
||||
<attr name="fastScrollThumbColorNormal" format="color" />
|
||||
<attr name="fastScrollTrackColorNormal" format="color" />
|
||||
<attr name="fastScrollThumbColorDragging" format="color" />
|
||||
<attr name="fastScrollTrackColorDragging" format="color" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue