From 4851f7f8fc5ca82d60a55e95730bf6c11675979c Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Wed, 5 Mar 2014 15:54:27 +0100 Subject: [PATCH 01/13] copy public keylist as stub for unified keylist --- OpenPGP-Keychain/src/main/AndroidManifest.xml | 16 +- .../keychain/ui/KeyListActivity.java | 71 ++++ .../keychain/ui/KeyListFragment.java | 338 ++++++++++++++++++ .../keychain/ui/adapter/KeyListAdapter.java | 232 ++++++++++++ .../src/main/res/layout/key_list_activity.xml | 20 ++ .../src/main/res/layout/key_list_fragment.xml | 77 ++++ .../src/main/res/menu/key_list.xml | 15 + .../src/main/res/menu/key_list_multi.xml | 12 + 8 files changed, 776 insertions(+), 5 deletions(-) create mode 100644 OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java create mode 100644 OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java create mode 100644 OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapter.java create mode 100644 OpenPGP-Keychain/src/main/res/layout/key_list_activity.xml create mode 100644 OpenPGP-Keychain/src/main/res/layout/key_list_fragment.xml create mode 100644 OpenPGP-Keychain/src/main/res/menu/key_list.xml create mode 100644 OpenPGP-Keychain/src/main/res/menu/key_list_multi.xml diff --git a/OpenPGP-Keychain/src/main/AndroidManifest.xml b/OpenPGP-Keychain/src/main/AndroidManifest.xml index 749229115..700e226e6 100644 --- a/OpenPGP-Keychain/src/main/AndroidManifest.xml +++ b/OpenPGP-Keychain/src/main/AndroidManifest.xml @@ -63,11 +63,6 @@ android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:label="@string/app_name" android:launchMode="singleTop"> - - - - - @@ -78,6 +73,17 @@ + + + + + + + + * + * 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; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.ExportHelper; + +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; + +public class KeyListActivity extends DrawerActivity { + + ExportHelper mExportHelper; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mExportHelper = new ExportHelper(this); + + setContentView(R.layout.key_list_public_activity); + + // now setup navigation drawer in DrawerActivity... + setupDrawerNavigation(savedInstanceState); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.key_list_public, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_key_list_public_import: + Intent intentImport = new Intent(this, ImportKeysActivity.class); + startActivityForResult(intentImport, 0); + + return true; + case R.id.menu_key_list_public_export: + mExportHelper.showExportKeysDialog(null, Id.type.public_key, Constants.path.APP_DIR + + "/pubexport.asc"); + + return true; + default: + return super.onOptionsItemSelected(item); + } + } + +} diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java new file mode 100644 index 000000000..6f94ffd1a --- /dev/null +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2013 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; + +import java.util.ArrayList; +import java.util.Set; + +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.adapter.KeyListAdapter; +import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; + +import se.emilsjolander.stickylistheaders.ApiLevelTooLowException; +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +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.ListView; +import android.widget.Toast; + +import com.beardedhen.androidbootstrap.BootstrapButton; + +/** + * 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 Fragment implements AdapterView.OnItemClickListener, + LoaderManager.LoaderCallbacks { + + private KeyListAdapter mAdapter; + private StickyListHeadersListView mStickyList; + + // empty list layout + private BootstrapButton mButtonEmptyCreate; + private BootstrapButton mButtonEmptyImport; + + /** + * Load custom layout with StickyListView from library + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.key_list_fragment, container, false); + + mButtonEmptyCreate = (BootstrapButton) view.findViewById(R.id.key_list_empty_button_create); + mButtonEmptyCreate.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent intent = new Intent(getActivity(), EditKeyActivity.class); + intent.setAction(EditKeyActivity.ACTION_CREATE_KEY); + intent.putExtra(EditKeyActivity.EXTRA_GENERATE_DEFAULT_KEYS, true); + intent.putExtra(EditKeyActivity.EXTRA_USER_IDS, ""); // show user id view + startActivityForResult(intent, 0); + } + }); + + mButtonEmptyImport = (BootstrapButton) view.findViewById(R.id.key_list_empty_button_import); + mButtonEmptyImport.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent intent = new Intent(getActivity(), ImportKeysActivity.class); + intent.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE); + startActivityForResult(intent, 0); + } + }); + + return view; + } + + /** + * Define Adapter and Loader on create of Activity + */ + @SuppressLint("NewApi") + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mStickyList = (StickyListHeadersListView) getActivity().findViewById(R.id.list); + + mStickyList.setOnItemClickListener(this); + mStickyList.setAreHeadersSticky(true); + mStickyList.setDrawingListUnderStickyHeader(false); + mStickyList.setFastScrollEnabled(true); + try { + mStickyList.setFastScrollAlwaysVisible(true); + } catch (ApiLevelTooLowException e) { + } + + // this view is made visible if no data is available + mStickyList.setEmptyView(getActivity().findViewById(R.id.empty)); + + /* + * ActionBarSherlock does not support MultiChoiceModeListener. Thus multi-selection is only + * available for Android >= 3.0 + */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mStickyList.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); + mStickyList.getWrappedList().setMultiChoiceModeListener(new MultiChoiceModeListener() { + + private int count = 0; + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + android.view.MenuInflater inflater = getActivity().getMenuInflater(); + inflater.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) { + Set positions = mAdapter.getCurrentCheckedPosition(); + + // get IDs for checked positions as long array + long[] ids = new long[positions.size()]; + int i = 0; + for (int pos : positions) { + ids[i] = mAdapter.getItemId(pos); + i++; + } + + switch (item.getItemId()) { + case R.id.menu_key_list_multi_encrypt: { + encrypt(mode, ids); + break; + } + case R.id.menu_key_list_multi_delete: { + showDeleteKeyDialog(mode, ids); + break; + } + } + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + count = 0; + mAdapter.clearSelection(); + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + if (checked) { + count++; + mAdapter.setNewSelection(position, checked); + } else { + count--; + mAdapter.removeSelection(position); + } + + String keysSelected = getResources().getQuantityString( + R.plurals.key_list_selected_keys, count, count); + mode.setTitle(keysSelected); + } + + }); + } + + // NOTE: Not supported by StickyListHeader, thus no indicator is shown while loading + // Start out with a progress indicator. + // setListShown(false); + + // Create an empty adapter we will use to display the loaded data. + mAdapter = new KeyListAdapter(getActivity(), null, Id.type.public_key, USER_ID_INDEX); + mStickyList.setAdapter(mAdapter); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); + } + + // These are the rows that we will retrieve. + static final String[] PROJECTION = new String[]{ + KeychainContract.KeyRings._ID, + KeychainContract.KeyRings.MASTER_KEY_ID, + KeychainContract.UserIds.USER_ID, + KeychainContract.Keys.IS_REVOKED + }; + + static final int USER_ID_INDEX = 2; + + static final String SORT_ORDER = UserIds.USER_ID + " ASC"; + + @Override + public Loader onCreateLoader(int id, Bundle args) { + // This is called when a new Loader needs to be created. This + // sample only has one Loader, so we don't care about the ID. + Uri baseUri = KeyRings.buildPublicKeyRingsUri(); + + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, SORT_ORDER); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.swapCursor(data); + + mStickyList.setAdapter(mAdapter); + + // NOTE: Not supported by StickyListHeader, thus no indicator is shown while loading + // The list should now be shown. + // if (isResumed()) { + // setListShown(true); + // } else { + // setListShownNoAnimation(true); + // } + } + + @Override + public void onLoaderReset(Loader loader) { + // 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); + } + + /** + * On click on item, start key view activity + */ + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + Intent viewIntent = null; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + viewIntent = new Intent(getActivity(), ViewKeyActivity.class); + } else { + viewIntent = new Intent(getActivity(), ViewKeyActivityJB.class); + } + viewIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(id))); + startActivity(viewIntent); + } + + @TargetApi(11) + public void encrypt(ActionMode mode, long[] keyRingRowIds) { + // get master key ids from row ids + long[] keyRingIds = new long[keyRingRowIds.length]; + for (int i = 0; i < keyRingRowIds.length; i++) { + keyRingIds[i] = ProviderHelper.getPublicMasterKeyId(getActivity(), keyRingRowIds[i]); + } + + Intent intent = new Intent(getActivity(), EncryptActivity.class); + intent.setAction(EncryptActivity.ACTION_ENCRYPT); + intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, keyRingIds); + // used instead of startActivity set actionbar based on callingPackage + startActivityForResult(intent, 0); + + mode.finish(); + } + + /** + * Show dialog to delete key + * + * @param keyRingRowIds + */ + @TargetApi(11) + public void showDeleteKeyDialog(final ActionMode mode, long[] keyRingRowIds) { + // Message is received after key is deleted + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) { + Bundle returnData = message.getData(); + if (returnData != null + && returnData.containsKey(DeleteKeyDialogFragment.MESSAGE_NOT_DELETED)) { + ArrayList notDeleted = + returnData.getStringArrayList(DeleteKeyDialogFragment.MESSAGE_NOT_DELETED); + String notDeletedMsg = ""; + for (String userId : notDeleted) { + notDeletedMsg += userId + "\n"; + } + Toast.makeText(getActivity(), getString(R.string.error_can_not_delete_contacts, notDeletedMsg) + + getResources().getQuantityString(R.plurals.error_can_not_delete_info, notDeleted.size()), + Toast.LENGTH_LONG).show(); + + mode.finish(); + } + } + } + }; + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(returnHandler); + + DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger, + keyRingRowIds, Id.type.public_key); + + deleteKeyDialog.show(getActivity().getSupportFragmentManager(), "deleteKeyDialog"); + } + +} diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapter.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapter.java new file mode 100644 index 000000000..2e0d1496b --- /dev/null +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapter.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2013 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.annotation.SuppressLint; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Color; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.HashMap; +import java.util.Set; + +import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; + +/** + * Implements StickyListHeadersAdapter from library + */ +public class KeyListAdapter extends CursorAdapter implements StickyListHeadersAdapter { + private LayoutInflater mInflater; + private int mSectionColumnIndex; + private int mIndexUserId; + private int mIndexIsRevoked; + + @SuppressLint("UseSparseArrays") + private HashMap mSelection = new HashMap(); + + public KeyListAdapter(Context context, Cursor c, int flags, int sectionColumnIndex) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + mSectionColumnIndex = sectionColumnIndex; + initIndex(c); + } + + @Override + public Cursor swapCursor(Cursor newCursor) { + initIndex(newCursor); + + return super.swapCursor(newCursor); + } + + /** + * Get column indexes for performance reasons just once in constructor and swapCursor. For a + * performance comparison see http://stackoverflow.com/a/17999582 + * + * @param cursor + */ + private void initIndex(Cursor cursor) { + if (cursor != null) { + mIndexUserId = cursor.getColumnIndexOrThrow(KeychainContract.UserIds.USER_ID); + mIndexIsRevoked = cursor.getColumnIndexOrThrow(KeychainContract.Keys.IS_REVOKED); + } + } + + /** + * Bind cursor data to the item list view + *

+ * NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method. Thus + * no ViewHolder is required here. + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); + TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); + TextView revoked = (TextView) view.findViewById(R.id.revoked); + + String userId = cursor.getString(mIndexUserId); + String[] userIdSplit = PgpKeyHelper.splitUserId(userId); + if (userIdSplit[0] != null) { + mainUserId.setText(userIdSplit[0]); + } else { + mainUserId.setText(R.string.user_id_no_name); + } + if (userIdSplit[1] != null) { + mainUserIdRest.setText(userIdSplit[1]); + mainUserIdRest.setVisibility(View.VISIBLE); + } else { + mainUserIdRest.setVisibility(View.GONE); + } + + boolean isRevoked = cursor.getInt(mIndexIsRevoked) > 0; + if (isRevoked) { + revoked.setVisibility(View.VISIBLE); + } else { + revoked.setVisibility(View.GONE); + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.key_list_item, null); + } + + /** + * 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.text = (TextView) convertView.findViewById(R.id.stickylist_header_text); + 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); + } + + // set header text as first char in user id + String userId = mCursor.getString(mSectionColumnIndex); + String headerText = convertView.getResources().getString(R.string.user_id_no_name); + if (userId != null && userId.length() > 0) { + headerText = "" + mCursor.getString(mSectionColumnIndex).subSequence(0, 1).charAt(0); + } + holder.text.setText(headerText); + 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); + } + + // return the first character of the name as ID because this is what + // headers are based upon + String userId = mCursor.getString(mSectionColumnIndex); + if (userId != null && userId.length() > 0) { + return userId.subSequence(0, 1).charAt(0); + } else { + return Long.MAX_VALUE; + } + } + + class HeaderViewHolder { + TextView text; + } + + /** + * -------------------------- MULTI-SELECTION METHODS -------------- + */ + public void setNewSelection(int position, boolean value) { + mSelection.put(position, value); + notifyDataSetChanged(); + } + + public boolean isPositionChecked(int position) { + Boolean result = mSelection.get(position); + return result == null ? false : result; + } + + public Set getCurrentCheckedPosition() { + return mSelection.keySet(); + } + + 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 + */ + // default color + v.setBackgroundColor(Color.TRANSPARENT); + if (mSelection.get(position) != null) { + // this is a selected position, change color! + v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis)); + } + return v; + } + +} diff --git a/OpenPGP-Keychain/src/main/res/layout/key_list_activity.xml b/OpenPGP-Keychain/src/main/res/layout/key_list_activity.xml new file mode 100644 index 000000000..65d246d7b --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/layout/key_list_activity.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/src/main/res/layout/key_list_fragment.xml b/OpenPGP-Keychain/src/main/res/layout/key_list_fragment.xml new file mode 100644 index 000000000..f3a21e1bf --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/layout/key_list_fragment.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/src/main/res/menu/key_list.xml b/OpenPGP-Keychain/src/main/res/menu/key_list.xml new file mode 100644 index 000000000..23ab8c9f3 --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/menu/key_list.xml @@ -0,0 +1,15 @@ + +

+ + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/src/main/res/menu/key_list_multi.xml b/OpenPGP-Keychain/src/main/res/menu/key_list_multi.xml new file mode 100644 index 000000000..bbed11f07 --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/menu/key_list_multi.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file From 8c6cb8b0ab9e89f9d575af829e0dd1dd9e82e401 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Sat, 8 Mar 2014 11:19:34 +0100 Subject: [PATCH 02/13] working unified list (no actions yet) --- .../keychain/provider/KeychainContract.java | 5 + .../keychain/provider/KeychainProvider.java | 75 +++++ .../keychain/ui/DrawerActivity.java | 2 +- .../keychain/ui/KeyListActivity.java | 8 +- .../keychain/ui/KeyListFragment.java | 275 +++++++++++++++++- .../keychain/ui/adapter/KeyListAdapter.java | 232 --------------- .../src/main/res/layout/key_list_header.xml | 16 + .../src/main/res/layout/key_list_item.xml | 63 ++++ .../main/res/layout/key_list_secret_item.xml | 20 +- 9 files changed, 450 insertions(+), 246 deletions(-) delete mode 100644 OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapter.java create mode 100644 OpenPGP-Keychain/src/main/res/layout/key_list_header.xml create mode 100644 OpenPGP-Keychain/src/main/res/layout/key_list_item.xml diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java index d8de30b37..706b30d05 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainContract.java @@ -78,6 +78,7 @@ public class KeychainContract { public static final String PATH_PUBLIC = "public"; public static final String PATH_SECRET = "secret"; + public static final String PATH_UNIFIED = "unified"; public static final String PATH_BY_MASTER_KEY_ID = "master_key_id"; public static final String PATH_BY_KEY_ID = "key_id"; @@ -100,6 +101,10 @@ public class KeychainContract { /** Use if a single item is returned */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.key_ring"; + public static Uri buildUnifiedKeyRingsUri() { + return CONTENT_URI.buildUpon().appendPath(PATH_UNIFIED).build(); + } + public static Uri buildPublicKeyRingsUri() { return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).build(); } diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java index 781f36758..85e01ae53 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -81,6 +81,8 @@ public class KeychainProvider extends ContentProvider { private static final int API_APPS_BY_ROW_ID = 302; private static final int API_APPS_BY_PACKAGE_NAME = 303; + private static final int UNIFIED_KEY_RING = 401; + // private static final int DATA_STREAM = 401; protected UriMatcher mUriMatcher; @@ -226,6 +228,16 @@ public class KeychainProvider extends ContentProvider { matcher.addURI(authority, KeychainContract.BASE_API_APPS + "/" + KeychainContract.PATH_BY_PACKAGE_NAME + "/*", API_APPS_BY_PACKAGE_NAME); + /** + * unified key rings + *
+         *
+         * key_rings/unified
+         *
+         */
+        matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/"
+                + KeychainContract.PATH_UNIFIED, UNIFIED_KEY_RING);
+
         /**
          * data stream
          *
@@ -455,6 +467,69 @@ public class KeychainProvider extends ContentProvider {
 
         int match = mUriMatcher.match(uri);
 
+        // screw that switch
+        if(match == UNIFIED_KEY_RING) {
+
+            // join keyrings with keys and userIds
+            // Only get user id and key with rank 0 (main user id and main key)
+            qb.setTables(Tables.KEY_RINGS + " INNER JOIN " + Tables.KEYS + " ON " + "("
+                    + Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.KEYS + "."
+                    + KeysColumns.KEY_RING_ROW_ID + " AND " + Tables.KEYS + "."
+                    + KeysColumns.RANK + " = '0') " + " INNER JOIN " + Tables.USER_IDS + " ON "
+                    + "(" + Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.USER_IDS + "."
+                    + UserIdsColumns.KEY_RING_ROW_ID + " AND " + Tables.USER_IDS + "."
+                    + UserIdsColumns.RANK + " = '0')");
+
+            {
+                HashMap projectionMap = new HashMap();
+
+                projectionMap.put(KeyRingsColumns.TYPE, "MAX(" + Tables.KEY_RINGS + "." + KeyRingsColumns.TYPE + ")");
+
+                projectionMap.put(BaseColumns._ID, Tables.KEY_RINGS + "." + BaseColumns._ID);
+                projectionMap.put(KeyRingsColumns.KEY_RING_DATA, Tables.KEY_RINGS + "."
+                        + KeyRingsColumns.KEY_RING_DATA);
+                projectionMap.put(KeyRingsColumns.MASTER_KEY_ID, Tables.KEY_RINGS + "." + KeyRingsColumns.MASTER_KEY_ID);
+                // TODO: deprecated master key id
+                //projectionMap.put(KeyRingsColumns.MASTER_KEY_ID, Tables.KEYS + "." + KeysColumns.KEY_ID);
+
+                projectionMap.put(KeysColumns.FINGERPRINT, Tables.KEYS + "." + KeysColumns.FINGERPRINT);
+                projectionMap.put(KeysColumns.IS_REVOKED, Tables.KEYS + "." + KeysColumns.IS_REVOKED);
+
+                projectionMap.put(UserIdsColumns.USER_ID, Tables.USER_IDS + "." + UserIdsColumns.USER_ID);
+
+                qb.setProjectionMap(projectionMap);
+            }
+
+            if (TextUtils.isEmpty(sortOrder)) {
+                sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
+            }
+
+            // If no sort order is specified use the default
+            String orderBy;
+            if (TextUtils.isEmpty(sortOrder)) {
+                orderBy = Tables.KEY_RINGS + "." + KeyRingsColumns.TYPE + " DESC";
+            } else {
+                orderBy = Tables.KEY_RINGS + "." + KeyRingsColumns.TYPE + " DESC, " + sortOrder;
+            }
+
+            Cursor c = qb.query(db, projection, selection, selectionArgs,
+                    Tables.KEY_RINGS + "." + KeyRingsColumns.MASTER_KEY_ID,
+                    null, orderBy);
+
+            // Tell the cursor what uri to watch, so it knows when its source data changes
+            c.setNotificationUri(getContext().getContentResolver(), uri);
+
+            if (Constants.DEBUG) {
+                Log.d(Constants.TAG,
+                        "Query: "
+                                + qb.buildQuery(projection, selection, selectionArgs, Tables.KEY_RINGS + "." + KeyRingsColumns.MASTER_KEY_ID, null,
+                                orderBy, null));
+                Log.d(Constants.TAG, "Cursor: " + DatabaseUtils.dumpCursorToString(c));
+            }
+
+            return c;
+        }
+
         switch (match) {
             case PUBLIC_KEY_RING:
             case SECRET_KEY_RING:
diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java
index 08ca262c3..d7644ce4d 100644
--- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java
+++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/DrawerActivity.java
@@ -50,7 +50,7 @@ public class DrawerActivity extends ActionBarActivity {
     private CharSequence mDrawerTitle;
     private CharSequence mTitle;
 
-    private static Class[] mItemsClass = new Class[] { KeyListPublicActivity.class,
+    private static Class[] mItemsClass = new Class[] { KeyListActivity.class,
             EncryptActivity.class, DecryptActivity.class, ImportKeysActivity.class,
             KeyListSecretActivity.class, RegisteredAppsListActivity.class };
     private Class mSelectedItem;
diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java
index dd7aa9a26..0bbe2edb1 100644
--- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java
+++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java
@@ -37,7 +37,7 @@ public class KeyListActivity extends DrawerActivity {
 
         mExportHelper = new ExportHelper(this);
 
-        setContentView(R.layout.key_list_public_activity);
+        setContentView(R.layout.key_list_activity);
 
         // now setup navigation drawer in DrawerActivity...
         setupDrawerNavigation(savedInstanceState);
@@ -46,19 +46,19 @@ public class KeyListActivity extends DrawerActivity {
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         super.onCreateOptionsMenu(menu);
-        getMenuInflater().inflate(R.menu.key_list_public, menu);
+        getMenuInflater().inflate(R.menu.key_list, menu);
         return true;
     }
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
-        case R.id.menu_key_list_public_import:
+        case R.id.menu_key_list_import:
             Intent intentImport = new Intent(this, ImportKeysActivity.class);
             startActivityForResult(intentImport, 0);
 
             return true;
-        case R.id.menu_key_list_public_export:
+        case R.id.menu_key_list_export:
             mExportHelper.showExportKeysDialog(null, Id.type.public_key, Constants.path.APP_DIR
                     + "/pubexport.asc");
 
diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
index 6f94ffd1a..a638796cd 100644
--- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
+++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
@@ -17,25 +17,32 @@
 
 package org.sufficientlysecure.keychain.ui;
 
+import java.util.HashMap;
 import java.util.ArrayList;
 import java.util.Set;
 
+import org.sufficientlysecure.keychain.Constants;
 import org.sufficientlysecure.keychain.Id;
 import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
 import org.sufficientlysecure.keychain.provider.KeychainContract;
 import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyTypes;
 import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
 import org.sufficientlysecure.keychain.provider.ProviderHelper;
-import org.sufficientlysecure.keychain.ui.adapter.KeyListAdapter;
 import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
 
 import se.emilsjolander.stickylistheaders.ApiLevelTooLowException;
 import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
+import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
 
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
+import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
+import android.graphics.Color;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -46,6 +53,7 @@ import android.support.v4.app.Fragment;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.content.CursorLoader;
 import android.support.v4.content.Loader;
+import android.support.v4.widget.CursorAdapter;
 import android.view.ActionMode;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -55,7 +63,9 @@ 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.Toast;
 
 import com.beardedhen.androidbootstrap.BootstrapButton;
@@ -207,7 +217,7 @@ public class KeyListFragment extends Fragment implements AdapterView.OnItemClick
         // setListShown(false);
 
         // Create an empty adapter we will use to display the loaded data.
-        mAdapter = new KeyListAdapter(getActivity(), null, Id.type.public_key, USER_ID_INDEX);
+        mAdapter = new KeyListAdapter(getActivity(), null, Id.type.public_key);
         mStickyList.setAdapter(mAdapter);
 
         // Prepare the loader. Either re-connect with an existing one,
@@ -218,20 +228,21 @@ public class KeyListFragment extends Fragment implements AdapterView.OnItemClick
     // These are the rows that we will retrieve.
     static final String[] PROJECTION = new String[]{
             KeychainContract.KeyRings._ID,
+            KeychainContract.KeyRings.TYPE,
             KeychainContract.KeyRings.MASTER_KEY_ID,
             KeychainContract.UserIds.USER_ID,
             KeychainContract.Keys.IS_REVOKED
     };
 
-    static final int USER_ID_INDEX = 2;
-
+    static final int INDEX_TYPE = 1;
+    static final int INDEX_UID = 3;
     static final String SORT_ORDER = UserIds.USER_ID + " ASC";
 
     @Override
     public Loader onCreateLoader(int id, Bundle args) {
         // This is called when a new Loader needs to be created. This
         // sample only has one Loader, so we don't care about the ID.
-        Uri baseUri = KeyRings.buildPublicKeyRingsUri();
+        Uri baseUri = KeyRings.buildUnifiedKeyRingsUri();
 
         // Now create and return a CursorLoader that will take care of
         // creating a Cursor for the data being displayed.
@@ -274,10 +285,14 @@ public class KeyListFragment extends Fragment implements AdapterView.OnItemClick
         } else {
             viewIntent = new Intent(getActivity(), ViewKeyActivityJB.class);
         }
-        viewIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(id)));
+        if(mAdapter.getKeyType(position) == KeyTypes.SECRET) {
+            viewIntent.setData(KeychainContract.KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(mAdapter.getMasterKeyId(position))));
+        } else {
+            viewIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsByMasterKeyIdUri(Long.toString(mAdapter.getMasterKeyId(position))));
+        }
         startActivity(viewIntent);
     }
-    
+
     @TargetApi(11)
     public void encrypt(ActionMode mode, long[] keyRingRowIds) {
         // get master key ids from row ids
@@ -335,4 +350,250 @@ public class KeyListFragment extends Fragment implements AdapterView.OnItemClick
         deleteKeyDialog.show(getActivity().getSupportFragmentManager(), "deleteKeyDialog");
     }
 
+    /**
+     * Implements StickyListHeadersAdapter from library
+     */
+    private class KeyListAdapter extends CursorAdapter implements StickyListHeadersAdapter {
+        private LayoutInflater mInflater;
+        private int mIndexUserId;
+        private int mIndexIsRevoked;
+        private int mMasterKeyId;
+
+        @SuppressLint("UseSparseArrays")
+        private HashMap mSelection = new HashMap();
+
+        public KeyListAdapter(Context context, Cursor c, int flags) {
+            super(context, c, flags);
+
+            mInflater = LayoutInflater.from(context);
+            initIndex(c);
+        }
+
+        @Override
+        public Cursor swapCursor(Cursor newCursor) {
+            initIndex(newCursor);
+
+            return super.swapCursor(newCursor);
+        }
+
+        /**
+         * Get column indexes for performance reasons just once in constructor and swapCursor. For a
+         * performance comparison see http://stackoverflow.com/a/17999582
+         *
+         * @param cursor
+         */
+        private void initIndex(Cursor cursor) {
+            if (cursor != null) {
+                mIndexUserId = cursor.getColumnIndexOrThrow(KeychainContract.UserIds.USER_ID);
+                mIndexIsRevoked = cursor.getColumnIndexOrThrow(KeychainContract.Keys.IS_REVOKED);
+                mMasterKeyId = cursor.getColumnIndexOrThrow(KeychainContract.KeyRings.MASTER_KEY_ID);
+            }
+        }
+
+        /**
+         * Bind cursor data to the item list view
+         * 

+ * NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method. Thus + * no ViewHolder is required here. + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + + { // set name and stuff, common to both key types + TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); + TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); + + String userId = cursor.getString(mIndexUserId); + String[] userIdSplit = PgpKeyHelper.splitUserId(userId); + if (userIdSplit[0] != null) { + mainUserId.setText(userIdSplit[0]); + } else { + mainUserId.setText(R.string.user_id_no_name); + } + if (userIdSplit[1] != null) { + mainUserIdRest.setText(userIdSplit[1]); + mainUserIdRest.setVisibility(View.VISIBLE); + } else { + mainUserIdRest.setVisibility(View.GONE); + } + } + + { // set edit button and revoked info, specific by key type + Button button = (Button) view.findViewById(R.id.edit); + TextView revoked = (TextView) view.findViewById(R.id.revoked); + + if(cursor.getInt(KeyListFragment.INDEX_TYPE) == KeyTypes.SECRET) { + // this is a secret key - show the edit button + revoked.setVisibility(View.GONE); + button.setVisibility(View.VISIBLE); + + final long id = cursor.getLong(mMasterKeyId); + button.setOnClickListener(new OnClickListener() { + public void onClick(View view) { + Log.d(Constants.TAG, "swag"); + Intent editIntent = new Intent(getActivity(), EditKeyActivity.class); + // editIntent.setData(KeychainContract.KeyRings.buildSecretKeyRingsUri(Long.toString(1))); + editIntent.setData(KeychainContract.KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(id))); + editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY); + startActivityForResult(editIntent, 0); + } + }); + } else { + // this is a public key - hide the edit button, show if it's revoked + button.setVisibility(View.GONE); + + boolean isRevoked = cursor.getInt(mIndexIsRevoked) > 0; + revoked.setVisibility(isRevoked ? View.VISIBLE : View.GONE); + } + } + + } + + public long getMasterKeyId(int id) { + + if (!mCursor.moveToPosition(id)) { + throw new IllegalStateException("couldn't move cursor to position " + id); + } + + return mCursor.getLong(mMasterKeyId); + + } + + public int getKeyType(int position) { + + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + return mCursor.getInt(KeyListFragment.INDEX_TYPE); + + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.key_list_item, parent, false); + } + + /** + * 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.text = (TextView) convertView.findViewById(R.id.stickylist_header_text); + 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_TYPE) == KeyTypes.SECRET) { + holder.text.setText("My Keys"); + return convertView; + } + + // set header text as first char in user id + String userId = mCursor.getString(KeyListFragment.INDEX_UID); + String headerText = convertView.getResources().getString(R.string.user_id_no_name); + if (userId != null && userId.length() > 0) { + headerText = "" + mCursor.getString(KeyListFragment.INDEX_UID).subSequence(0, 1).charAt(0); + } + holder.text.setText(headerText); + 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_TYPE) == KeyTypes.SECRET) + return 1L; + + // otherwise, return the first character of the name as ID + String userId = mCursor.getString(KeyListFragment.INDEX_UID); + if (userId != null && userId.length() > 0) { + return userId.charAt(0); + } else { + return Long.MAX_VALUE; + } + } + + class HeaderViewHolder { + TextView text; + } + + /** + * -------------------------- MULTI-SELECTION METHODS -------------- + */ + public void setNewSelection(int position, boolean value) { + mSelection.put(position, value); + notifyDataSetChanged(); + } + + public boolean isPositionChecked(int position) { + Boolean result = mSelection.get(position); + return result == null ? false : result; + } + + public Set getCurrentCheckedPosition() { + return mSelection.keySet(); + } + + 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 + */ + // default color + v.setBackgroundColor(Color.TRANSPARENT); + if (mSelection.get(position) != null) { + // this is a selected position, change color! + v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis)); + } + return v; + } + + } + } diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapter.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapter.java deleted file mode 100644 index 2e0d1496b..000000000 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapter.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (C) 2013 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.annotation.SuppressLint; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Color; -import android.support.v4.widget.CursorAdapter; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.util.Log; - -import java.util.HashMap; -import java.util.Set; - -import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; - -/** - * Implements StickyListHeadersAdapter from library - */ -public class KeyListAdapter extends CursorAdapter implements StickyListHeadersAdapter { - private LayoutInflater mInflater; - private int mSectionColumnIndex; - private int mIndexUserId; - private int mIndexIsRevoked; - - @SuppressLint("UseSparseArrays") - private HashMap mSelection = new HashMap(); - - public KeyListAdapter(Context context, Cursor c, int flags, int sectionColumnIndex) { - super(context, c, flags); - - mInflater = LayoutInflater.from(context); - mSectionColumnIndex = sectionColumnIndex; - initIndex(c); - } - - @Override - public Cursor swapCursor(Cursor newCursor) { - initIndex(newCursor); - - return super.swapCursor(newCursor); - } - - /** - * Get column indexes for performance reasons just once in constructor and swapCursor. For a - * performance comparison see http://stackoverflow.com/a/17999582 - * - * @param cursor - */ - private void initIndex(Cursor cursor) { - if (cursor != null) { - mIndexUserId = cursor.getColumnIndexOrThrow(KeychainContract.UserIds.USER_ID); - mIndexIsRevoked = cursor.getColumnIndexOrThrow(KeychainContract.Keys.IS_REVOKED); - } - } - - /** - * Bind cursor data to the item list view - *

- * NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method. Thus - * no ViewHolder is required here. - */ - @Override - public void bindView(View view, Context context, Cursor cursor) { - TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); - TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); - TextView revoked = (TextView) view.findViewById(R.id.revoked); - - String userId = cursor.getString(mIndexUserId); - String[] userIdSplit = PgpKeyHelper.splitUserId(userId); - if (userIdSplit[0] != null) { - mainUserId.setText(userIdSplit[0]); - } else { - mainUserId.setText(R.string.user_id_no_name); - } - if (userIdSplit[1] != null) { - mainUserIdRest.setText(userIdSplit[1]); - mainUserIdRest.setVisibility(View.VISIBLE); - } else { - mainUserIdRest.setVisibility(View.GONE); - } - - boolean isRevoked = cursor.getInt(mIndexIsRevoked) > 0; - if (isRevoked) { - revoked.setVisibility(View.VISIBLE); - } else { - revoked.setVisibility(View.GONE); - } - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return mInflater.inflate(R.layout.key_list_item, null); - } - - /** - * 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.text = (TextView) convertView.findViewById(R.id.stickylist_header_text); - 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); - } - - // set header text as first char in user id - String userId = mCursor.getString(mSectionColumnIndex); - String headerText = convertView.getResources().getString(R.string.user_id_no_name); - if (userId != null && userId.length() > 0) { - headerText = "" + mCursor.getString(mSectionColumnIndex).subSequence(0, 1).charAt(0); - } - holder.text.setText(headerText); - 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); - } - - // return the first character of the name as ID because this is what - // headers are based upon - String userId = mCursor.getString(mSectionColumnIndex); - if (userId != null && userId.length() > 0) { - return userId.subSequence(0, 1).charAt(0); - } else { - return Long.MAX_VALUE; - } - } - - class HeaderViewHolder { - TextView text; - } - - /** - * -------------------------- MULTI-SELECTION METHODS -------------- - */ - public void setNewSelection(int position, boolean value) { - mSelection.put(position, value); - notifyDataSetChanged(); - } - - public boolean isPositionChecked(int position) { - Boolean result = mSelection.get(position); - return result == null ? false : result; - } - - public Set getCurrentCheckedPosition() { - return mSelection.keySet(); - } - - 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 - */ - // default color - v.setBackgroundColor(Color.TRANSPARENT); - if (mSelection.get(position) != null) { - // this is a selected position, change color! - v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis)); - } - return v; - } - -} diff --git a/OpenPGP-Keychain/src/main/res/layout/key_list_header.xml b/OpenPGP-Keychain/src/main/res/layout/key_list_header.xml new file mode 100644 index 000000000..5768e4153 --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/layout/key_list_header.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/src/main/res/layout/key_list_item.xml b/OpenPGP-Keychain/src/main/res/layout/key_list_item.xml new file mode 100644 index 000000000..4ff0bbe65 --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/layout/key_list_item.xml @@ -0,0 +1,63 @@ + + + + + + + +