diff --git a/OpenKeychain/build.gradle b/OpenKeychain/build.gradle index 7f92d3811..459a7b12d 100644 --- a/OpenKeychain/build.gradle +++ b/OpenKeychain/build.gradle @@ -2,7 +2,9 @@ apply plugin: 'com.android.application' apply plugin: 'witness' dependencies { + // NOTE: Always use fixed version codes not dynamic ones, e.g. 0.7.3 instead of 0.7.+, see README for more information + // NOTE: libraries are pinned to a specific build, see below // from local Android SDK @@ -10,7 +12,6 @@ dependencies { compile 'com.android.support:appcompat-v7:22.0.0' compile 'com.android.support:recyclerview-v7:22.0.0' compile 'com.android.support:cardview-v7:22.0.0' - // JCenter etc. compile 'com.eftimoff:android-patternview:1.0.1@aar' compile 'com.journeyapps:zxing-android-embedded:2.1.0@aar' @@ -20,7 +21,7 @@ dependencies { compile 'it.neokree:MaterialNavigationDrawer:1.3.2' compile 'com.getbase:floatingactionbutton:1.9.0' compile 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.0' - + compile 'com.tonicartos:superslim:0.4.8' // libs as submodules compile project(':extern:openpgp-api-lib') compile project(':extern:openkeychain-api-lib') diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java index 5035c2793..70df81d1d 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -25,8 +25,6 @@ import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.database.Cursor; -import android.graphics.Color; -import android.graphics.PorterDuff; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -37,9 +35,10 @@ import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; -import android.support.v4.widget.CursorAdapter; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.view.ActionMode; +import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SearchView; -import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -47,15 +46,11 @@ 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.ImageButton; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.TextView; +import android.widget.LinearLayout; 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; @@ -64,20 +59,18 @@ import org.sufficientlysecure.keychain.operations.results.ConsolidateResult; import org.sufficientlysecure.keychain.operations.results.DeleteResult; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; -import org.sufficientlysecure.keychain.pgp.KeyRing; import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.service.CloudImportService; import org.sufficientlysecure.keychain.service.KeychainIntentService; -import org.sufficientlysecure.keychain.service.ServiceProgressHandler; import org.sufficientlysecure.keychain.service.PassphraseCacheService; +import org.sufficientlysecure.keychain.service.ServiceProgressHandler; +import org.sufficientlysecure.keychain.ui.adapter.KeyListAdapter; import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; -import org.sufficientlysecure.keychain.ui.util.Highlighter; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; -import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils.State; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.ExportHelper; import org.sufficientlysecure.keychain.util.FabContainer; @@ -86,26 +79,22 @@ import org.sufficientlysecure.keychain.util.Preferences; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; - -import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; -import se.emilsjolander.stickylistheaders.StickyListHeadersListView; /** * 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, - LoaderManager.LoaderCallbacks, FabContainer { + implements SearchView.OnQueryTextListener, KeyListAdapter.OnClickListener, + LoaderManager.LoaderCallbacks, FabContainer, ActionMode.Callback { static final int REQUEST_REPEAT_PASSPHRASE = 1; static final int REQUEST_ACTION = 2; ExportHelper mExportHelper; + private RecyclerView mRecyclerView; private KeyListAdapter mAdapter; - private StickyListHeadersListView mStickyList; // saves the mode object for multiselect, needed for reset at some point private ActionMode mActionMode = null; @@ -134,8 +123,9 @@ public class KeyListFragment extends LoaderFragment View root = super.onCreateView(inflater, superContainer, savedInstanceState); View view = inflater.inflate(R.layout.key_list_fragment, getContainer()); - mStickyList = (StickyListHeadersListView) view.findViewById(R.id.key_list_list); - mStickyList.setOnItemClickListener(this); + mRecyclerView = (RecyclerView) view.findViewById(R.id.key_list_recycler); + mRecyclerView.setHasFixedSize(true); + mRecyclerView.setLayoutManager(new LayoutManager(getActivity())); mFab = (FloatingActionsMenu) view.findViewById(R.id.fab_main); @@ -180,103 +170,6 @@ public class KeyListFragment extends LoaderFragment // show app name instead of "keys" from nav drawer getActivity().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(getActivity()); - - 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 = getActivity().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(mode, ids, mAdapter.isAnySecretSelected()); - break; - } - case R.id.menu_key_list_multi_export: { - ids = mAdapter.getCurrentSelectedMasterKeyIds(); - showMultiExportDialog(ids); - break; - } - case R.id.menu_key_list_multi_select_all: { - // select all - for (int i = 0; i < mStickyList.getCount(); i++) { - mStickyList.setItemChecked(i, true); - } - 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); @@ -284,8 +177,9 @@ public class KeyListFragment extends LoaderFragment setContentShown(false); // Create an empty adapter we will use to display the loaded data. - mAdapter = new KeyListAdapter(getActivity(), null, 0); - mStickyList.setAdapter(mAdapter); + mAdapter = new KeyListAdapter(getActivity()); + mAdapter.setOnClickListener(this); + mRecyclerView.setAdapter(mAdapter); // Prepare the loader. Either re-connect with an existing one, // or start a new one. @@ -304,14 +198,6 @@ public class KeyListFragment extends LoaderFragment KeyRings.HAS_DUPLICATE_USER_ID, }; - static final int INDEX_MASTER_KEY_ID = 1; - static final int INDEX_USER_ID = 2; - static final int INDEX_IS_REVOKED = 3; - static final int INDEX_IS_EXPIRED = 4; - static final int INDEX_VERIFIED = 5; - static final int INDEX_HAS_ANY_SECRET = 6; - static final int INDEX_HAS_DUPLICATE_USER_ID = 7; - static final String ORDER = KeyRings.HAS_ANY_SECRET + " DESC, UPPER(" + KeyRings.USER_ID + ") ASC"; @@ -349,10 +235,17 @@ public class KeyListFragment extends LoaderFragment mAdapter.setSearchQuery(mQuery); mAdapter.swapCursor(data); - mStickyList.setAdapter(mAdapter); + mRecyclerView.setAdapter(mAdapter); // this view is made visible if no data is available - mStickyList.setEmptyView(getActivity().findViewById(R.id.key_list_empty)); + LinearLayout emptyLayout = (LinearLayout) getActivity().findViewById(R.id.key_list_empty); + if (mAdapter.getItemCount() == 0) { + mRecyclerView.setVisibility(View.GONE); + emptyLayout.setVisibility(View.VISIBLE); + } else { + emptyLayout.setVisibility(View.GONE); + mRecyclerView.setVisibility(View.VISIBLE); + } // end action mode, if any if (mActionMode != null) { @@ -375,15 +268,38 @@ public class KeyListFragment extends LoaderFragment mAdapter.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))); - startActivity(viewIntent); + public void onImportClick() { + importFile(); + } + + @Override + public void onKeySlingerClick(KeyListAdapter.KeyHolder holder, int position) { + Intent safeSlingerIntent = new Intent(getActivity(), SafeSlingerActivity.class); + safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, holder.getMasterKeyId()); + startActivityForResult(safeSlingerIntent, 0); + } + + @Override + public void onKeyClick(KeyListAdapter.KeyHolder holder, int position) { + if (mActionMode == null) { + Intent viewIntent = new Intent(getActivity(), ViewKeyActivity.class); + viewIntent.setData(KeychainContract.KeyRings.buildGenericKeyRingUri(holder.getMasterKeyId())); + startActivity(viewIntent); + } else { + mAdapter.toggleSelection(position); + updateSelectionCount(); + } + } + + @Override + public void onKeyLongClick(KeyListAdapter.KeyHolder holder, int position) { + if (mActionMode == null) { + ((ActionBarActivity) getActivity()).startSupportActionMode(this); + } + + mAdapter.toggleSelection(position); + updateSelectionCount(); } protected void encrypt(ActionMode mode, long[] masterKeyIds) { @@ -787,292 +703,62 @@ public class KeyListFragment extends LoaderFragment anim.start(); } - /** - * Implements StickyListHeadersAdapter from library - */ - private class KeyListAdapter extends CursorAdapter implements StickyListHeadersAdapter { - private String mQuery; - private LayoutInflater mInflater; - - private HashMap mSelection = new HashMap<>(); - - public KeyListAdapter(Context context, Cursor c, int flags) { - super(context, c, flags); - - mInflater = LayoutInflater.from(context); - } - - public void setSearchQuery(String query) { - mQuery = query; - } - - @Override - public Cursor swapCursor(Cursor newCursor) { - return super.swapCursor(newCursor); - } - - private class ItemViewHolder { - Long mMasterKeyId; - TextView mMainUserId; - TextView mMainUserIdRest; - ImageView mStatus; - View mSlinger; - ImageButton mSlingerButton; - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - View view = mInflater.inflate(R.layout.key_list_item, parent, false); - final ItemViewHolder holder = new ItemViewHolder(); - holder.mMainUserId = (TextView) view.findViewById(R.id.key_list_item_name); - holder.mMainUserIdRest = (TextView) view.findViewById(R.id.key_list_item_email); - holder.mStatus = (ImageView) view.findViewById(R.id.key_list_item_status_icon); - holder.mSlinger = view.findViewById(R.id.key_list_item_slinger_view); - holder.mSlingerButton = (ImageButton) view.findViewById(R.id.key_list_item_slinger_button); - holder.mSlingerButton.setColorFilter(context.getResources().getColor(R.color.tertiary_text_light), - PorterDuff.Mode.SRC_IN); - view.setTag(holder); - view.findViewById(R.id.key_list_item_slinger_button).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (holder.mMasterKeyId != null) { - Intent safeSlingerIntent = new Intent(getActivity(), SafeSlingerActivity.class); - safeSlingerIntent.putExtra(SafeSlingerActivity.EXTRA_MASTER_KEY_ID, holder.mMasterKeyId); - startActivityForResult(safeSlingerIntent, REQUEST_ACTION); - } - } - }); - return view; - } - - /** - * Bind cursor data to the item list view - */ - @Override - public void bindView(View view, Context context, Cursor cursor) { - Highlighter highlighter = new Highlighter(context, mQuery); - ItemViewHolder h = (ItemViewHolder) view.getTag(); - - { // set name and stuff, common to both key types - String userId = cursor.getString(INDEX_USER_ID); - KeyRing.UserId userIdSplit = KeyRing.splitUserId(userId); - if (userIdSplit.name != null) { - h.mMainUserId.setText(highlighter.highlight(userIdSplit.name)); - } else { - h.mMainUserId.setText(R.string.user_id_no_name); - } - if (userIdSplit.email != null) { - h.mMainUserIdRest.setText(highlighter.highlight(userIdSplit.email)); - h.mMainUserIdRest.setVisibility(View.VISIBLE); - } else { - h.mMainUserIdRest.setVisibility(View.GONE); - } - } - - { // set edit button and status, specific by key type - - long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); - boolean isSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) != 0; - boolean isRevoked = cursor.getInt(INDEX_IS_REVOKED) > 0; - boolean isExpired = cursor.getInt(INDEX_IS_EXPIRED) != 0; - boolean isVerified = cursor.getInt(INDEX_VERIFIED) > 0; - boolean hasDuplicate = cursor.getInt(INDEX_HAS_DUPLICATE_USER_ID) == 1; - - h.mMasterKeyId = masterKeyId; - - // Note: order is important! - if (isRevoked) { - KeyFormattingUtils.setStatusImage(getActivity(), h.mStatus, null, State.REVOKED, R.color.bg_gray); - h.mStatus.setVisibility(View.VISIBLE); - h.mSlinger.setVisibility(View.GONE); - h.mMainUserId.setTextColor(context.getResources().getColor(R.color.bg_gray)); - h.mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.bg_gray)); - } else if (isExpired) { - KeyFormattingUtils.setStatusImage(getActivity(), h.mStatus, null, State.EXPIRED, R.color.bg_gray); - h.mStatus.setVisibility(View.VISIBLE); - h.mSlinger.setVisibility(View.GONE); - h.mMainUserId.setTextColor(context.getResources().getColor(R.color.bg_gray)); - h.mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.bg_gray)); - } else if (isSecret) { - h.mStatus.setVisibility(View.GONE); - h.mSlinger.setVisibility(View.VISIBLE); - h.mMainUserId.setTextColor(context.getResources().getColor(R.color.black)); - h.mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.black)); - } else { - // this is a public key - show if it's verified - if (isVerified) { - KeyFormattingUtils.setStatusImage(getActivity(), h.mStatus, State.VERIFIED); - h.mStatus.setVisibility(View.VISIBLE); - } else { - KeyFormattingUtils.setStatusImage(getActivity(), h.mStatus, State.UNVERIFIED); - h.mStatus.setVisibility(View.VISIBLE); - } - h.mSlinger.setVisibility(View.GONE); - h.mMainUserId.setTextColor(context.getResources().getColor(R.color.black)); - h.mMainUserIdRest.setTextColor(context.getResources().getColor(R.color.black)); - } - } - - } - - public boolean isSecretAvailable(int id) { - if (!mCursor.moveToPosition(id)) { - throw new IllegalStateException("couldn't move cursor to position " + id); - } - - return mCursor.getInt(INDEX_HAS_ANY_SECRET) != 0; - } - - public long getMasterKeyId(int id) { - if (!mCursor.moveToPosition(id)) { - throw new IllegalStateException("couldn't move cursor to position " + id); - } - - return mCursor.getLong(INDEX_MASTER_KEY_ID); - } - - /** - * Creates a new header view and binds the section headers to it. It uses the ViewHolder - * pattern. Most functionality is similar to getView() from Android's CursorAdapter. - *

- * NOTE: The variables mDataValid and mCursor are available due to the super class - * CursorAdapter. - */ - @Override - public View getHeaderView(int position, View convertView, ViewGroup parent) { - HeaderViewHolder holder; - if (convertView == null) { - holder = new HeaderViewHolder(); - convertView = mInflater.inflate(R.layout.key_list_header, parent, false); - holder.mText = (TextView) convertView.findViewById(R.id.stickylist_header_text); - holder.mCount = (TextView) convertView.findViewById(R.id.contacts_num); - convertView.setTag(holder); - } else { - holder = (HeaderViewHolder) convertView.getTag(); - } - - if (!mDataValid) { - // no data available at this point - Log.d(Constants.TAG, "getHeaderView: No data available at this point!"); - return convertView; - } - - if (!mCursor.moveToPosition(position)) { - throw new IllegalStateException("couldn't move cursor to position " + position); - } - - if (mCursor.getInt(KeyListFragment.INDEX_HAS_ANY_SECRET) != 0) { - { // set contact count - int num = mCursor.getCount(); - String contactsTotal = 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(KeyListFragment.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(KeyListFragment.INDEX_HAS_ANY_SECRET) != 0) { - return 1L; - } - // otherwise, return the first character of the name as ID - String userId = mCursor.getString(KeyListFragment.INDEX_USER_ID); - if (userId != null && userId.length() > 0) { - return Character.toUpperCase(userId.charAt(0)); - } else { - return Long.MAX_VALUE; - } - } - - private class HeaderViewHolder { - TextView mText; - TextView mCount; - } - - /** - * -------------------------- MULTI-SELECTION METHODS -------------- - */ - public void setNewSelection(int position, boolean value) { - mSelection.put(position, value); - notifyDataSetChanged(); - } - - public boolean isAnySecretSelected() { - for (int pos : mSelection.keySet()) { - if (mAdapter.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++] = mAdapter.getMasterKeyId(pos); - } - return ids; - } - - public void removeSelection(int position) { - mSelection.remove(position); - notifyDataSetChanged(); - } - - public void clearSelection() { - mSelection.clear(); - notifyDataSetChanged(); - } - - @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); - - /** - * Change color for multi-selection - */ - if (mSelection.get(position) != null) { - // selected position color - v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis)); - } else { - // default color - v.setBackgroundColor(Color.TRANSPARENT); - } - - return v; - } - + @Override + public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { + MenuInflater inflater = getActivity().getMenuInflater(); + inflater.inflate(R.menu.key_list_multi, menu); + mActionMode = actionMode; + return true; } + @Override + public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { + // get IDs for checked positions as long array + long[] ids; + + switch (menuItem.getItemId()) { + case R.id.menu_key_list_multi_encrypt: { + ids = mAdapter.getCurrentSelectedMasterKeyIds(); + encrypt(actionMode, ids); + break; + } + case R.id.menu_key_list_multi_delete: { + ids = mAdapter.getCurrentSelectedMasterKeyIds(); + showDeleteKeyDialog(actionMode, ids, mAdapter.isAnySecretSelected()); + break; + } + case R.id.menu_key_list_multi_export: { + ids = mAdapter.getCurrentSelectedMasterKeyIds(); + showMultiExportDialog(ids); + break; + } + case R.id.menu_key_list_multi_select_all: { + mAdapter.selectAll(); + updateSelectionCount(); + break; + } + } + return true; + } + + @Override + public void onDestroyActionMode(ActionMode actionMode) { + mActionMode = null; + mAdapter.clearSelection(); + } + + private void updateSelectionCount() { + int count = mAdapter.getSelectionCount(); + if (count == 0) { + mActionMode.finish(); + } else { + mActionMode.setTitle(getResources().getQuantityString(R.plurals.key_list_selected_keys, count, count)); + } + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapter.java new file mode 100644 index 000000000..724a82c2b --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapter.java @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.tonicartos.superslim.LayoutManager; +import com.tonicartos.superslim.LinearSLM; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.KeyRing; +import org.sufficientlysecure.keychain.ui.util.Highlighter; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; + +import java.util.ArrayList; + +public class KeyListAdapter extends RecyclerCursorAdapter { + + static final int INDEX_MASTER_KEY_ID = 1; + static final int INDEX_USER_ID = 2; + static final int INDEX_IS_REVOKED = 3; + static final int INDEX_IS_EXPIRED = 4; + static final int INDEX_VERIFIED = 5; + static final int INDEX_HAS_ANY_SECRET = 6; + static final int INDEX_HAS_DUPLICATE_USER_ID = 7; + + private Context mContext; + private ArrayList mItemList = new ArrayList<>(); + private OnClickListener mOnClickListener; + private String mQuery; + + public KeyListAdapter(Context context) { + super(null); + + mContext = context; + + setOnCursorSwappedListener(new OnCursorSwappedListener() { + + @Override + public void onCursorSwapped(Cursor oldCursor, Cursor newCursor) { + mItemList = new ArrayList<>(); + + if (newCursor == null) { + return; + } + + int count = newCursor.getCount(); + if (count == 0) { + return; + } + + mItemList.add(new Item(Item.TYPE_MY_KEYS_HEADER, 0)); + + int i; + for (i = 0; i < count; i++) { + if (newCursor.moveToPosition(i) && newCursor.getInt(INDEX_HAS_ANY_SECRET) != 0) { + mItemList.add(new Item(Item.TYPE_KEY, 0, i)); + } else { + break; + } + } + + if (mItemList.size() == 1) { + if (mQuery == null || mQuery.isEmpty()) { + mItemList.add(new Item(Item.TYPE_IMPORT, 0)); + } else { + mItemList.clear(); + } + } + + char prevHeaderChar = '\0'; + int prevHeaderIndex = 0; + for (; i < count; i++) { + if (newCursor.moveToPosition(i)) { + char headerChar = Character.toUpperCase(newCursor.getString(INDEX_USER_ID).charAt(0)); + + if (headerChar != prevHeaderChar) { + prevHeaderChar = headerChar; + prevHeaderIndex = mItemList.size(); + + mItemList.add(new Item(Item.TYPE_CHAR_HEADER, prevHeaderIndex)); + } + + mItemList.add(new Item(Item.TYPE_KEY, prevHeaderIndex, i)); + } + } + + mItemList.add(new Item(Item.TYPE_FOOTER, mItemList.size() - 1)); + } + + }); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + RecyclerView.ViewHolder viewHolder = null; + switch (viewType) { + case Item.TYPE_MY_KEYS_HEADER: + + case Item.TYPE_CHAR_HEADER: + viewHolder = new HeaderHolder(LayoutInflater.from(mContext) + .inflate(R.layout.key_list_header, parent, false)); + break; + + case Item.TYPE_IMPORT: + viewHolder = new ImportKeyHolder(LayoutInflater.from(mContext) + .inflate(R.layout.key_list_import, parent, false)); + break; + + case Item.TYPE_KEY: + viewHolder = new KeyHolder(LayoutInflater.from(mContext) + .inflate(R.layout.key_list_key, parent, false)); + break; + + case Item.TYPE_FOOTER: + viewHolder = new RecyclerView.ViewHolder(LayoutInflater.from(mContext) + .inflate(R.layout.key_list_footer, parent, false)) { + }; + break; + } + + return viewHolder; + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + Item item = mItemList.get(position); + switch (item.mType) { + case Item.TYPE_MY_KEYS_HEADER: + + case Item.TYPE_CHAR_HEADER: + ((HeaderHolder) holder).bind(position); + break; + + case Item.TYPE_KEY: + ((KeyHolder) holder).bind(position); + break; + } + + View itemView = holder.itemView; + LayoutManager.LayoutParams layoutParams = new LayoutManager.LayoutParams(itemView.getLayoutParams()); + layoutParams.setSlm(LinearSLM.ID); + layoutParams.setFirstPosition(item.mHeaderIndex); + itemView.setLayoutParams(layoutParams); + } + + @Override + public int getItemViewType(int position) { + return mItemList.get(position).mType; + } + + @Override + public int getItemCount() { + return mItemList.size(); + } + + public void toggleSelection(int position) { + Item item = mItemList.get(position); + item.mSelected = item.mSelectable && !item.mSelected; + + notifyDataSetChanged(); + } + + public void selectAll() { + for (Item item : mItemList) { + item.mSelected = item.mSelectable; + } + + notifyDataSetChanged(); + } + + public void clearSelection() { + for (Item item : mItemList) { + item.mSelected = false; + } + + notifyDataSetChanged(); + } + + public int getSelectionCount() { + int count = 0; + for (Item item : mItemList) { + if (item.mSelected) { + count++; + } + } + + return count; + } + + public boolean isAnySecretSelected() { + for (Item item : mItemList) { + if (item.mSelected + && mCursor.moveToPosition(item.mCursorPosition) + && mCursor.getInt(INDEX_HAS_ANY_SECRET) != 0) { + return true; + } + } + + return false; + } + + public long[] getCurrentSelectedMasterKeyIds() { + long[] ids = new long[getSelectionCount()]; + int i = 0; + for (Item item : mItemList) { + if (item.mSelected + && mCursor.moveToPosition(item.mCursorPosition)) { + ids[i++] = mCursor.getLong(INDEX_MASTER_KEY_ID); + } + } + + return ids; + } + + public void setOnClickListener(OnClickListener onClickListener) { + mOnClickListener = onClickListener; + } + + public void setSearchQuery(String query) { + mQuery = query; + } + + private class Item { + + public static final int TYPE_MY_KEYS_HEADER = 0; + public static final int TYPE_CHAR_HEADER = 1; + public static final int TYPE_IMPORT = 2; + public static final int TYPE_KEY = 3; + public static final int TYPE_FOOTER = 4; + + private int mType, mHeaderIndex, mCursorPosition; + private boolean mSelectable, mSelected; + + private Item(int type, int headerIndex, int cursorPosition) { + mType = type; + mHeaderIndex = headerIndex; + mCursorPosition = cursorPosition; + mSelectable = cursorPosition != -1; + } + + private Item(int type, int headerIndex) { + this(type, headerIndex, -1); + } + + } + + public class HeaderHolder extends RecyclerView.ViewHolder { + + private TextView mTextView, mCountTextView; + + private HeaderHolder(View itemView) { + super(itemView); + + mTextView = (TextView) itemView.findViewById(R.id.key_list_header_text); + mCountTextView = (TextView) itemView.findViewById(R.id.key_list_header_count); + } + + private void bind(int position) { + Item item = mItemList.get(position); + switch (item.mType) { + case Item.TYPE_MY_KEYS_HEADER: { + mTextView.setText(mContext.getString(R.string.my_keys)); + + int count = mCursor.getCount(); + mCountTextView.setText(mContext.getResources().getQuantityString(R.plurals.n_keys, count, count)); + mCountTextView.setVisibility(View.VISIBLE); + break; + } + + case Item.TYPE_CHAR_HEADER: { + Item nextItem = mItemList.get(position + 1); + mCursor.moveToPosition(nextItem.mCursorPosition); + + String userId = mCursor.getString(INDEX_USER_ID), + text = mContext.getString(R.string.user_id_no_name); + if (userId != null && !userId.isEmpty()) { + text = String.valueOf(Character.toUpperCase(userId.charAt(0))); + } + mTextView.setText(text); + + if (position == 0) { + int count = mCursor.getCount(); + mCountTextView.setText(mContext.getResources().getQuantityString(R.plurals.n_keys, count, count)); + mCountTextView.setVisibility(View.VISIBLE); + } else { + mCountTextView.setVisibility(View.GONE); + } + break; + } + } + } + + } + + public class ImportKeyHolder extends RecyclerView.ViewHolder { + + public ImportKeyHolder(View itemView) { + super(itemView); + + itemView.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + mOnClickListener.onImportClick(); + } + + }); + } + + } + + public class KeyHolder extends RecyclerView.ViewHolder { + + private View mDividerView; + private TextView mNameTextView, mEmailTextView; + private LinearLayout mSlingerLayout; + private ImageButton mSlingerImageButton; + private ImageView mStatusImageView; + + private int mPosition; + private long mMasterKeyId; + + private KeyHolder(View itemView) { + super(itemView); + + mDividerView = itemView.findViewById(R.id.key_list_key_divider); + mNameTextView = (TextView) itemView.findViewById(R.id.key_list_key_name); + mEmailTextView = (TextView) itemView.findViewById(R.id.key_list_key_email); + mSlingerLayout = (LinearLayout) itemView.findViewById(R.id.key_list_key_slinger_view); + mSlingerImageButton = (ImageButton) itemView.findViewById(R.id.key_list_key_slinger_button); + mStatusImageView = (ImageView) itemView.findViewById(R.id.key_list_key_status_icon); + + itemView.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + mOnClickListener.onKeyClick(KeyHolder.this, mPosition); + } + + }); + + itemView.setOnLongClickListener(new View.OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + mOnClickListener.onKeyLongClick(KeyHolder.this, mPosition); + return true; + } + + }); + + mSlingerImageButton.setColorFilter(mContext.getResources().getColor(R.color.tertiary_text_light), + PorterDuff.Mode.SRC_IN); + mSlingerImageButton.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + mOnClickListener.onKeySlingerClick(KeyHolder.this, mPosition); + } + + }); + } + + private void bind(int position) { + mPosition = position; + + Item item = mItemList.get(position); + mCursor.moveToPosition(item.mCursorPosition); + + if (item.mSelected) { + // selected position color + itemView.setBackgroundColor(itemView.getResources().getColor(R.color.emphasis)); + } else { + // default color + itemView.setBackgroundColor(Color.TRANSPARENT); + } + + Item prevItem = mItemList.get(position - 1); + if (prevItem.mType == Item.TYPE_MY_KEYS_HEADER + || prevItem.mType == Item.TYPE_CHAR_HEADER) { + mDividerView.setVisibility(View.GONE); + } else { + mDividerView.setVisibility(View.VISIBLE); + } + + Highlighter highlighter = new Highlighter(mContext, mQuery); + + // set name and stuff, common to both key types + String userId = mCursor.getString(INDEX_USER_ID); + KeyRing.UserId userIdSplit = KeyRing.splitUserId(userId); + if (userIdSplit.name != null) { + mNameTextView.setText(highlighter.highlight(userIdSplit.name)); + } else { + mNameTextView.setText(R.string.user_id_no_name); + } + if (userIdSplit.email != null) { + mEmailTextView.setText(highlighter.highlight(userIdSplit.email)); + mEmailTextView.setVisibility(View.VISIBLE); + } else { + mEmailTextView.setVisibility(View.GONE); + } + + // set edit button and status, specific by key type + long masterKeyId = mCursor.getLong(INDEX_MASTER_KEY_ID); + boolean isSecret = mCursor.getInt(INDEX_HAS_ANY_SECRET) != 0; + boolean isRevoked = mCursor.getInt(INDEX_IS_REVOKED) > 0; + boolean isExpired = mCursor.getInt(INDEX_IS_EXPIRED) != 0; + boolean isVerified = mCursor.getInt(INDEX_VERIFIED) > 0; + boolean hasDuplicate = mCursor.getInt(INDEX_HAS_DUPLICATE_USER_ID) == 1; + + mMasterKeyId = masterKeyId; + + // Note: order is important! + if (isRevoked) { + KeyFormattingUtils.setStatusImage(mContext, mStatusImageView, null, KeyFormattingUtils.State.REVOKED, R.color.bg_gray); + mStatusImageView.setVisibility(View.VISIBLE); + mSlingerLayout.setVisibility(View.GONE); + mNameTextView.setTextColor(mContext.getResources().getColor(R.color.bg_gray)); + mEmailTextView.setTextColor(mContext.getResources().getColor(R.color.bg_gray)); + } else if (isExpired) { + KeyFormattingUtils.setStatusImage(mContext, mStatusImageView, null, KeyFormattingUtils.State.EXPIRED, R.color.bg_gray); + mStatusImageView.setVisibility(View.VISIBLE); + mSlingerLayout.setVisibility(View.GONE); + mNameTextView.setTextColor(mContext.getResources().getColor(R.color.bg_gray)); + mEmailTextView.setTextColor(mContext.getResources().getColor(R.color.bg_gray)); + } else if (isSecret) { + mStatusImageView.setVisibility(View.GONE); + mSlingerLayout.setVisibility(View.VISIBLE); + mNameTextView.setTextColor(mContext.getResources().getColor(R.color.black)); + mEmailTextView.setTextColor(mContext.getResources().getColor(R.color.black)); + } else { + // this is a public key - show if it's verified + if (isVerified) { + KeyFormattingUtils.setStatusImage(mContext, mStatusImageView, KeyFormattingUtils.State.VERIFIED); + mStatusImageView.setVisibility(View.VISIBLE); + } else { + KeyFormattingUtils.setStatusImage(mContext, mStatusImageView, KeyFormattingUtils.State.UNVERIFIED); + mStatusImageView.setVisibility(View.VISIBLE); + } + mSlingerLayout.setVisibility(View.GONE); + mNameTextView.setTextColor(mContext.getResources().getColor(R.color.black)); + mEmailTextView.setTextColor(mContext.getResources().getColor(R.color.black)); + } + } + + public long getMasterKeyId() { + return mMasterKeyId; + } + + } + + public interface OnClickListener { + + public void onImportClick(); + + public void onKeySlingerClick(KeyHolder holder, int position); + + public void onKeyClick(KeyHolder holder, int position); + + public void onKeyLongClick(KeyHolder holder, int position); + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/RecyclerCursorAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/RecyclerCursorAdapter.java new file mode 100644 index 000000000..e884e0bf2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/RecyclerCursorAdapter.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013-2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import android.database.Cursor; +import android.support.v7.widget.RecyclerView; + +public abstract class RecyclerCursorAdapter extends RecyclerView.Adapter { + + protected Cursor mCursor; + private OnCursorSwappedListener mOnCursorSwappedListener; + + public RecyclerCursorAdapter(Cursor cursor) { + mCursor = cursor; + + notifyDataSetChanged(); + } + + /** + * Swap in a new Cursor, returning the old Cursor. The returned old Cursor is not closed. + * + * @param newCursor The new cursor to be used. + * @return Returns the previously set Cursor, or null if there was not one. + * If the given new Cursor is the same instance is the previously set + * Cursor, null is also returned. + */ + public Cursor swapCursor(Cursor newCursor) { + if (newCursor == mCursor) { + return null; + } + + Cursor oldCursor = mCursor; + mCursor = newCursor; + + mOnCursorSwappedListener.onCursorSwapped(oldCursor, newCursor); + + notifyDataSetChanged(); + + return oldCursor; + } + + public void setOnCursorSwappedListener(OnCursorSwappedListener mOnCursorSwappedListener) { + this.mOnCursorSwappedListener = mOnCursorSwappedListener; + } + + public interface OnCursorSwappedListener { + + public void onCursorSwapped(Cursor oldCursor, Cursor newCursor); + + } + +} diff --git a/OpenKeychain/src/main/res/layout/key_list_footer.xml b/OpenKeychain/src/main/res/layout/key_list_footer.xml new file mode 100644 index 000000000..60eb5c365 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/key_list_footer.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/key_list_fragment.xml b/OpenKeychain/src/main/res/layout/key_list_fragment.xml index ea3426f90..69d64f514 100644 --- a/OpenKeychain/src/main/res/layout/key_list_fragment.xml +++ b/OpenKeychain/src/main/res/layout/key_list_fragment.xml @@ -10,14 +10,13 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_height="wrap_content" + android:background="@color/background_material_light" + app:slm_headerDisplay="inline|sticky" + app:slm_isHeader="true"> + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/key_list_item.xml b/OpenKeychain/src/main/res/layout/key_list_item.xml deleted file mode 100644 index db0462c6d..000000000 --- a/OpenKeychain/src/main/res/layout/key_list_item.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/OpenKeychain/src/main/res/layout/key_list_key.xml b/OpenKeychain/src/main/res/layout/key_list_key.xml new file mode 100644 index 000000000..db41eedec --- /dev/null +++ b/OpenKeychain/src/main/res/layout/key_list_key.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/menu/key_list_multi.xml b/OpenKeychain/src/main/res/menu/key_list_multi.xml index 7fdf4a5c1..d9ee57dce 100644 --- a/OpenKeychain/src/main/res/menu/key_list_multi.xml +++ b/OpenKeychain/src/main/res/menu/key_list_multi.xml @@ -1,6 +1,7 @@

+ xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index 6ee30887b..8bf0edc66 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -85,6 +85,7 @@ "Encrypt files" "Encrypt text" "Add additional email address" + Import from file "Settings" diff --git a/extern/StickyListHeaders b/extern/StickyListHeaders deleted file mode 160000 index 70a2ed806..000000000 --- a/extern/StickyListHeaders +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 70a2ed80632938540bf07b81270384f4e5a96f9e