diff --git a/OpenPGP-Keychain/src/main/AndroidManifest.xml b/OpenPGP-Keychain/src/main/AndroidManifest.xml index 4fabf7432..b1fbcf555 100644 --- a/OpenPGP-Keychain/src/main/AndroidManifest.xml +++ b/OpenPGP-Keychain/src/main/AndroidManifest.xml @@ -61,7 +61,7 @@ android:theme="@style/KeychainTheme" android:label="@string/app_name"> @@ -71,12 +71,6 @@ - - + android:parentActivityName=".ui.KeyListActivity"> + android:value=".ui.KeyListActivity" /> + android:parentActivityName=".ui.KeyListActivity"> + android:value=".ui.KeyListActivity" /> --> - \ No newline at end of file + 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 3b60ffc87..8b0efdbbd 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/provider/ProviderHelper.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
index dd538fbf4..1c05344d6 100644
--- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
+++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/ProviderHelper.java
@@ -483,13 +483,13 @@ public class ProviderHelper {
      */
     public static boolean getSecretMasterKeyCanSign(Context context, long keyRingRowId) {
         Uri queryUri = KeyRings.buildSecretKeyRingsUri(String.valueOf(keyRingRowId));
-        return getMasterKeyCanSign(context, queryUri, keyRingRowId);
+        return getMasterKeyCanSign(context, queryUri);
     }
 
     /**
      * Private helper method to get master key private empty status of keyring by its row id
      */
-    private static boolean getMasterKeyCanSign(Context context, Uri queryUri, long keyRingRowId) {
+    public static boolean getMasterKeyCanSign(Context context, Uri queryUri) {
         String[] projection = new String[]{
                 KeyRings.MASTER_KEY_ID,
                 "(SELECT COUNT(sign_keys." + Keys._ID + ") FROM " + Tables.KEYS
@@ -515,6 +515,12 @@ public class ProviderHelper {
         return (masterKeyId > 0);
     }
 
+    public static boolean hasSecretKeyByMasterKeyId(Context context, long masterKeyId) {
+        Uri queryUri = KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
+        // see if we can get our master key id back from the uri
+        return getMasterKeyId(context, queryUri) == masterKeyId;
+    }
+
     /**
      * Get master key id of keyring by its row id
      */
@@ -546,6 +552,26 @@ public class ProviderHelper {
         return masterKeyId;
     }
 
+    public static long getRowId(Context context, Uri queryUri) {
+        String[] projection = new String[]{KeyRings._ID};
+        Cursor cursor = context.getContentResolver().query(queryUri, projection, null, null, null);
+
+        long rowId = 0;
+        try {
+            if (cursor != null && cursor.moveToFirst()) {
+                int idCol = cursor.getColumnIndexOrThrow(KeyRings._ID);
+
+                rowId = cursor.getLong(idCol);
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        return rowId;
+    }
+
     /**
      * Get fingerprint of key
      */
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..ebb520197 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,9 +50,9 @@ 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 };
+            RegisteredAppsListActivity.class };
     private Class mSelectedItem;
 
     private static final int MENU_ID_PREFERENCE = 222;
@@ -72,7 +72,6 @@ public class DrawerActivity extends ActionBarActivity {
                 new NavItem("fa-lock", getString(R.string.nav_encrypt)),
                 new NavItem("fa-unlock", getString(R.string.nav_decrypt)),
                 new NavItem("fa-download", getString(R.string.nav_import)),
-                new NavItem("fa-key", getString(R.string.nav_secret_keys)),
                 new NavItem("fa-android", getString(R.string.nav_apps)) };
 
         mDrawerList.setAdapter(new NavigationDrawerAdapter(this, R.layout.drawer_list_item,
diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java
index 72dc97ccd..0c35bb2b1 100644
--- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java
+++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/EditKeyActivity.java
@@ -266,12 +266,10 @@ public class EditKeyActivity extends ActionBarActivity {
         } else {
             Log.d(Constants.TAG, "uri: " + mDataUri);
 
-            long keyRingRowId = Long.valueOf(mDataUri.getLastPathSegment());
-
             // get master key id using row id
-            long masterKeyId = ProviderHelper.getSecretMasterKeyId(this, keyRingRowId);
+            long masterKeyId = ProviderHelper.getMasterKeyId(this, mDataUri);
 
-            masterCanSign = ProviderHelper.getSecretMasterKeyCanSign(this, keyRingRowId);
+            masterCanSign = ProviderHelper.getMasterKeyCanSign(this, mDataUri);
             finallyEdit(masterKeyId, masterCanSign);
         }
     }
diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java
similarity index 87%
rename from OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java
rename to OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java
index 9bd9ee225..9eebbed64 100644
--- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java
+++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListActivity.java
@@ -27,7 +27,7 @@ import android.os.Bundle;
 import android.view.Menu;
 import android.view.MenuItem;
 
-public class KeyListPublicActivity extends DrawerActivity {
+public class KeyListActivity extends DrawerActivity {
 
     ExportHelper mExportHelper;
 
@@ -37,7 +37,7 @@ public class KeyListPublicActivity 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,20 @@ public class KeyListPublicActivity 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:
+            // TODO fix this for unified keylist
             mExportHelper.showExportKeysDialog(null, Id.type.public_key, Constants.path.APP_DIR_FILE_PUB);
 
             return true;
diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
similarity index 54%
rename from OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java
rename to OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
index de965daa5..5b57132d4 100644
--- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java
+++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListFragment.java
@@ -17,26 +17,34 @@
 
 package org.sufficientlysecure.keychain.ui;
 
+import java.util.HashMap;
 import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 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.helper.ExportHelper;
 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.KeyListPublicAdapter;
 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;
@@ -47,10 +55,13 @@ 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.support.v4.view.MenuItemCompat;
 import android.support.v7.app.ActionBarActivity;
 import android.support.v7.widget.SearchView;
+import android.text.Spannable;
 import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
 import android.view.ActionMode;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -62,7 +73,9 @@ import android.view.ViewGroup;
 import android.view.animation.AnimationUtils;
 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;
@@ -71,10 +84,10 @@ 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 KeyListPublicFragment extends Fragment implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener,
+public class KeyListFragment extends Fragment implements SearchView.OnQueryTextListener, AdapterView.OnItemClickListener,
         LoaderManager.LoaderCallbacks {
 
-    private KeyListPublicAdapter mAdapter;
+    private KeyListAdapter mAdapter;
     private StickyListHeadersListView mStickyList;
 
     // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097
@@ -94,9 +107,9 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
      */
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-        View root = inflater.inflate(R.layout.key_list_public_fragment, container, false);
+        View root = inflater.inflate(R.layout.key_list_fragment, container, false);
 
-        mStickyList = (StickyListHeadersListView) root.findViewById(R.id.key_list_public_list);
+        mStickyList = (StickyListHeadersListView) root.findViewById(R.id.key_list_list);
         mStickyList.setOnItemClickListener(this);
 
 
@@ -125,8 +138,8 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
         });
 
         // rebuild functionality of ListFragment, http://stackoverflow.com/a/12504097
-        mListContainer = root.findViewById(R.id.key_list_public_list_container);
-        mProgressContainer = root.findViewById(R.id.key_list_public_progress_container);
+        mListContainer = root.findViewById(R.id.key_list_list_container);
+        mProgressContainer = root.findViewById(R.id.key_list_progress_container);
         mListShown = true;
 
         return root;
@@ -140,6 +153,7 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
+        mStickyList.setOnItemClickListener(this);
         mStickyList.setAreHeadersSticky(true);
         mStickyList.setDrawingListUnderStickyHeader(false);
         mStickyList.setFastScrollEnabled(true);
@@ -149,7 +163,7 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
         }
 
         // this view is made visible if no data is available
-        mStickyList.setEmptyView(getActivity().findViewById(R.id.key_list_public_empty));
+        mStickyList.setEmptyView(getActivity().findViewById(R.id.key_list_empty));
 
         /*
          * ActionBarSherlock does not support MultiChoiceModeListener. Thus multi-selection is only
@@ -162,7 +176,7 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
                 @Override
                 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                     android.view.MenuInflater inflater = getActivity().getMenuInflater();
-                    inflater.inflate(R.menu.key_list_public_multi, menu);
+                    inflater.inflate(R.menu.key_list_multi, menu);
                     return true;
                 }
 
@@ -173,30 +187,21 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
 
                 @Override
                 public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-                    // get row ids for checked positions as long array
-                    long[] ids = mStickyList.getCheckedItemIds();
+
+                    // get IDs for checked positions as long array
+                    long[] ids;
 
                     switch (item.getItemId()) {
-                        case R.id.menu_key_list_public_multi_encrypt: {
+                        case R.id.menu_key_list_multi_encrypt: {
+                            ids = mAdapter.getCurrentSelectedMasterKeyIds();
                             encrypt(mode, ids);
                             break;
                         }
-                        case R.id.menu_key_list_public_multi_export: {
-                            ExportHelper mExportHelper = new ExportHelper((ActionBarActivity) getActivity());
-                            mExportHelper.showExportKeysDialog(ids, Id.type.public_key, Constants.path.APP_DIR_FILE_PUB);
-                            break;
-                        }
-                        case R.id.menu_key_list_public_multi_delete: {
+                        case R.id.menu_key_list_multi_delete: {
+                            ids = mStickyList.getWrappedList().getCheckedItemIds();
                             showDeleteKeyDialog(mode, ids);
                             break;
                         }
-                        case R.id.menu_key_list_public_multi_select_all: {
-                            //Select all
-                            for (int i = 0; i < mStickyList.getCount(); i++) {
-                                mStickyList.setItemChecked(i, true);
-                            }
-                            break;
-                        }
                     }
                     return true;
                 }
@@ -231,7 +236,7 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
         setListShown(false);
 
         // Create an empty adapter we will use to display the loaded data.
-        mAdapter = new KeyListPublicAdapter(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,
@@ -242,20 +247,21 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
     // 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();
         String where = null;
         String whereArgs[] = null;
         if (mCurQuery != null) {
@@ -273,6 +279,7 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
         // old cursor once we return.)
         mAdapter.setSearchQuery(mCurQuery);
         mAdapter.swapCursor(data);
+
         mStickyList.setAdapter(mAdapter);
 
         // NOTE: Not supported by StickyListHeader, but reimplemented here
@@ -303,21 +310,15 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
         } else {
             viewIntent = new Intent(getActivity(), ViewKeyActivityJB.class);
         }
-        viewIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(id)));
+        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
-        long[] keyRingIds = new long[keyRingRowIds.length];
-        for (int i = 0; i < keyRingRowIds.length; i++) {
-            keyRingIds[i] = ProviderHelper.getPublicMasterKeyId(getActivity(), keyRingRowIds[i]);
-        }
-
+    protected void encrypt(ActionMode mode, long[] keyRingMasterKeyIds) {
         Intent intent = new Intent(getActivity(), EncryptActivity.class);
         intent.setAction(EncryptActivity.ACTION_ENCRYPT);
-        intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, keyRingIds);
+        intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, keyRingMasterKeyIds);
         // used instead of startActivity set actionbar based on callingPackage
         startActivityForResult(intent, 0);
 
@@ -330,6 +331,7 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
      * @param keyRingRowIds
      */
     @TargetApi(11)
+    // TODO: this method needs an overhaul to handle both public and secret keys gracefully!
     public void showDeleteKeyDialog(final ActionMode mode, long[] keyRingRowIds) {
         // Message is received after key is deleted
         Handler returnHandler = new Handler() {
@@ -368,7 +370,7 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
     @Override
     public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
         // Get the searchview
-        MenuItem searchItem = menu.findItem(R.id.menu_key_list_public_search);
+        MenuItem searchItem = menu.findItem(R.id.menu_key_list_search);
         mSearchView = (SearchView) MenuItemCompat.getActionView(searchItem);
 
         // Execute this when searching
@@ -385,7 +387,7 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
             public boolean onMenuItemActionCollapse(MenuItem item) {
                 mCurQuery = null;
                 mSearchView.setQuery("", true);
-                getLoaderManager().restartLoader(0, null, KeyListPublicFragment.this);
+                getLoaderManager().restartLoader(0, null, KeyListFragment.this);
                 return true;
             }
         });
@@ -444,4 +446,289 @@ public class KeyListPublicFragment extends Fragment implements SearchView.OnQuer
     public void setListShownNoAnimation(boolean shown) {
         setListShown(shown, false);
     }
+
+    /**
+     * Implements StickyListHeadersAdapter from library
+     */
+    private class KeyListAdapter extends CursorAdapter implements StickyListHeadersAdapter {
+        private LayoutInflater mInflater;
+        private int mIndexUserId;
+        private int mIndexIsRevoked;
+        private int mMasterKeyId;
+
+        private String mCurQuery;
+
+        @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(highlightSearchQuery(userIdSplit[0])); + } else { + mainUserId.setText(R.string.user_id_no_name); + } + if (userIdSplit[1] != null) { + mainUserIdRest.setText(highlightSearchQuery(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) { + Intent editIntent = new Intent(getActivity(), EditKeyActivity.class); + 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); + holder.count = (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_TYPE) == KeyTypes.SECRET) { + { // set contact count + int num = mCursor.getCount(); + String contactsTotal = getResources().getQuantityString(R.plurals.n_contacts, num, num); + holder.count.setText(contactsTotal); + holder.count.setVisibility(View.VISIBLE); + } + + holder.text.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_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); + holder.count.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_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; + TextView count; + } + + /** + * -------------------------- MULTI-SELECTION METHODS -------------- + */ + public void setNewSelection(int position, boolean value) { + mSelection.put(position, value); + notifyDataSetChanged(); + } + + 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 + */ + // 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; + } + + // search highlight methods + + public void setSearchQuery(String searchQuery) { + mCurQuery = searchQuery; + } + + public String getSearchQuery() { + return mCurQuery; + } + + protected Spannable highlightSearchQuery(String text) { + Spannable highlight = Spannable.Factory.getInstance().newSpannable(text); + + if (mCurQuery != null) { + Pattern pattern = Pattern.compile("(?i)" + mCurQuery); + Matcher matcher = pattern.matcher(text); + if (matcher.find()) { + highlight.setSpan( + new ForegroundColorSpan(mContext.getResources().getColor(R.color.emphasis)), + matcher.start(), + matcher.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return highlight; + } else { + return highlight; + } + } + } + } diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java deleted file mode 100644 index cd3c8b4fd..000000000 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java +++ /dev/null @@ -1,93 +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; - -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 KeyListSecretActivity extends DrawerActivity { - - ExportHelper mExportHelper; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mExportHelper = new ExportHelper(this); - - setContentView(R.layout.key_list_secret_activity); - - // now setup navigation drawer in DrawerActivity... - setupDrawerNavigation(savedInstanceState); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - getMenuInflater().inflate(R.menu.key_list_secret, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_key_list_secret_create: - createKey(); - - return true; - case R.id.menu_key_list_secret_create_expert: - createKeyExpert(); - - return true; - case R.id.menu_key_list_secret_export: - mExportHelper.showExportKeysDialog(null, Id.type.secret_key, Constants.path.APP_DIR_FILE_SEC); - - return true; - case R.id.menu_key_list_secret_import: - Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class); - intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_KEY_FROM_FILE); - startActivityForResult(intentImportFromFile, 0); - - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void createKey() { - Intent intent = new Intent(this, 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); - } - - private void createKeyExpert() { - Intent intent = new Intent(this, EditKeyActivity.class); - intent.setAction(EditKeyActivity.ACTION_CREATE_KEY); - startActivityForResult(intent, 0); - } - -} diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java deleted file mode 100644 index e84b2f4c8..000000000 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.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; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.Id; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.helper.ExportHelper; -import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; -import org.sufficientlysecure.keychain.ui.adapter.KeyListSecretAdapter; -import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; - -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.ListFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v7.app.ActionBarActivity; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ListView; -import android.widget.AbsListView.MultiChoiceModeListener; -import android.widget.AdapterView.OnItemClickListener; - -public class KeyListSecretFragment extends ListFragment implements - LoaderManager.LoaderCallbacks, OnItemClickListener { - - private KeyListSecretAdapter mAdapter; - - /** - * Define Adapter and Loader on create of Activity - */ - @SuppressLint("NewApi") - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - getListView().setOnItemClickListener(this); - - // Give some text to display if there is no data. In a real - // application this would come from a resource. - setEmptyText(getString(R.string.list_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) { - getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); - getListView().setMultiChoiceModeListener(new MultiChoiceModeListener() { - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - android.view.MenuInflater inflater = getActivity().getMenuInflater(); - inflater.inflate(R.menu.key_list_secret_multi, menu); - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - // get row ids for checked positions as long array - long[] ids = getListView().getCheckedItemIds(); - - switch (item.getItemId()) { - case R.id.menu_key_list_public_multi_delete: { - showDeleteKeyDialog(mode, ids); - break; - } - case R.id.menu_key_list_public_multi_select_all: { - //Select all - for (int i = 0; i < getListView().getCount(); i++) { - getListView().setItemChecked(i, true); - } - break; - } - case R.id.menu_key_list_public_multi_export: { - ExportHelper mExportHelper = new ExportHelper((ActionBarActivity) getActivity()); - mExportHelper.showExportKeysDialog(ids, Id.type.secret_key, Constants.path.APP_DIR_FILE_SEC); - break; - } - - } - return true; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - mAdapter.clearSelection(); - } - - @Override - public void onItemCheckedStateChanged(ActionMode mode, int position, long id, - boolean checked) { - if (checked) { - mAdapter.setNewSelection(position, checked); - } else { - mAdapter.removeSelection(position); - } - - int count = getListView().getCheckedItemCount(); - String keysSelected = getResources().getQuantityString( - R.plurals.key_list_selected_keys, count, count); - mode.setTitle(keysSelected); - } - - }); - } - - // We have a menu item to show in action bar. - setHasOptionsMenu(true); - - // Start out with a progress indicator. - setListShown(false); - - // Create an empty adapter we will use to display the loaded data. - mAdapter = new KeyListSecretAdapter(getActivity(), null, 0); - setListAdapter(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[]{KeyRings._ID, KeyRings.MASTER_KEY_ID, - UserIds.USER_ID}; - static final String SORT_ORDER = UserIds.USER_ID + " COLLATE LOCALIZED ASC"; - - 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. - // First, pick the base URI to use depending on whether we are - // currently filtering. - Uri baseUri = KeyRings.buildSecretKeyRingsUri(); - - // 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); - } - - 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); - - // The list should now be shown. - if (isResumed()) { - setListShown(true); - } else { - setListShownNoAnimation(true); - } - } - - 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 editIntent = new Intent(getActivity(), EditKeyActivity.class); - editIntent.setData(KeychainContract.KeyRings.buildSecretKeyRingsUri(Long.toString(id))); - editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY); - startActivityForResult(editIntent, 0); - } - - /** - * 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) { - mode.finish(); - } - } - }; - - // Create a new Messenger for the communication back - Messenger messenger = new Messenger(returnHandler); - - DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger, - keyRingRowIds, Id.type.secret_key); - - deleteKeyDialog.show(getActivity().getSupportFragmentManager(), "deleteKeyDialog"); - } - -} diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index 4cc5e1b62..581d14a22 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -36,6 +36,7 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; import org.sufficientlysecure.keychain.helper.ExportHelper; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.ui.adapter.TabsAdapter; import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; @@ -83,7 +84,13 @@ public class ViewKeyActivity extends ActionBarActivity { selectedTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB); } - mDataUri = getIntent().getData(); + { + // normalize mDataUri to a "by row id" query, to ensure it works with any + // given valid /public/ query + long rowId = ProviderHelper.getRowId(this, getIntent().getData()); + // TODO: handle (rowId == 0) with something else than a crash + mDataUri = KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(rowId)) ; + } Bundle mainBundle = new Bundle(); mainBundle.putParcelable(ViewKeyMainFragment.ARG_DATA_URI, mDataUri); @@ -107,7 +114,7 @@ public class ViewKeyActivity extends ActionBarActivity { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: - Intent homeIntent = new Intent(this, KeyListPublicActivity.class); + Intent homeIntent = new Intent(this, KeyListActivity.class); homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(homeIntent); return true; diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java index adb06a068..4950126dd 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java @@ -63,6 +63,8 @@ public class ViewKeyMainFragment extends Fragment implements private TextView mExpiry; private TextView mCreation; private TextView mFingerprint; + private TextView mSecretKey; + private BootstrapButton mActionEdit; private BootstrapButton mActionEncrypt; private ListView mUserIds; @@ -89,8 +91,10 @@ public class ViewKeyMainFragment extends Fragment implements mCreation = (TextView) view.findViewById(R.id.creation); mExpiry = (TextView) view.findViewById(R.id.expiry); mFingerprint = (TextView) view.findViewById(R.id.fingerprint); + mSecretKey = (TextView) view.findViewById(R.id.secret_key); mUserIds = (ListView) view.findViewById(R.id.user_ids); mKeys = (ListView) view.findViewById(R.id.keys); + mActionEdit = (BootstrapButton) view.findViewById(R.id.action_edit); mActionEncrypt = (BootstrapButton) view.findViewById(R.id.action_encrypt); return view; @@ -120,6 +124,31 @@ public class ViewKeyMainFragment extends Fragment implements Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); + { // label whether secret key is available, and edit button if it is + final long masterKeyId = ProviderHelper.getMasterKeyId(getActivity(), mDataUri); + if(ProviderHelper.hasSecretKeyByMasterKeyId(getActivity(), masterKeyId)) { + // set this attribute. this is a LITTLE unclean, but we have the info available + // right here, so why not. + mSecretKey.setTextColor(getResources().getColor(R.color.emphasis)); + mSecretKey.setText(R.string.secret_key_yes); + + // edit button + mActionEdit.setVisibility(View.VISIBLE); + mActionEdit.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + Intent editIntent = new Intent(getActivity(), EditKeyActivity.class); + editIntent.setData(KeychainContract.KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId))); + editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY); + startActivityForResult(editIntent, 0); + } + }); + } else { + mSecretKey.setTextColor(Color.BLACK); + mSecretKey.setText(getResources().getString(R.string.secret_key_no)); + mActionEdit.setVisibility(View.GONE); + } + } + mActionEncrypt.setOnClickListener(new View.OnClickListener() { @Override diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java deleted file mode 100644 index d7bb62d01..000000000 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java +++ /dev/null @@ -1,221 +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 java.util.HashMap; - -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 se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.database.Cursor; -import android.graphics.Color; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -/** - * Implements StickyListHeadersAdapter from library - */ -public class KeyListPublicAdapter extends HighlightQueryCursorAdapter implements StickyListHeadersAdapter { - private LayoutInflater mInflater; - private int mSectionColumnIndex; - private int mIndexUserId; - private int mIndexIsRevoked; - - @SuppressLint("UseSparseArrays") - private HashMap mSelection = new HashMap(); - - public KeyListPublicAdapter(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(highlightSearchQuery(userIdSplit[0])); - } else { - mainUserId.setText(R.string.user_id_no_name); - } - if (userIdSplit[1] != null) { - mainUserIdRest.setText(highlightSearchQuery(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_public_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_public_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 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 && mSelection.get(position).booleanValue()) { - // color for selected items - v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis)); - } else { - // default color - v.setBackgroundColor(Color.TRANSPARENT); - } - return v; - } - -} diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListSecretAdapter.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListSecretAdapter.java deleted file mode 100644 index 0bffcaa19..000000000 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/KeyListSecretAdapter.java +++ /dev/null @@ -1,129 +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 java.util.HashMap; - -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; - -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; - -public class KeyListSecretAdapter extends CursorAdapter { - private LayoutInflater mInflater; - - private int mIndexUserId; - - @SuppressLint("UseSparseArrays") - private HashMap mSelection = new HashMap(); - - public KeyListSecretAdapter(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(UserIds.USER_ID); - } - } - - @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); - - 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]); - } else { - mainUserIdRest.setText(""); - } - } - - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return mInflater.inflate(R.layout.key_list_secret_item, null); - } - - /** -------------------------- MULTI-SELECTION METHODS -------------- */ - public void setNewSelection(int position, boolean value) { - mSelection.put(position, value); - notifyDataSetChanged(); - } - - 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) { - // color for selected items - v.setBackgroundColor(parent.getResources().getColor(R.color.emphasis)); - } else { - // default color - v.setBackgroundColor(Color.TRANSPARENT); - } - return v; - } - -} diff --git a/OpenPGP-Keychain/src/main/res/layout/key_list_public_activity.xml b/OpenPGP-Keychain/src/main/res/layout/key_list_activity.xml similarity index 89% rename from OpenPGP-Keychain/src/main/res/layout/key_list_public_activity.xml rename to OpenPGP-Keychain/src/main/res/layout/key_list_activity.xml index f0e843e56..65d246d7b 100644 --- a/OpenPGP-Keychain/src/main/res/layout/key_list_public_activity.xml +++ b/OpenPGP-Keychain/src/main/res/layout/key_list_activity.xml @@ -9,8 +9,8 @@ android:layout_height="match_parent" > diff --git a/OpenPGP-Keychain/src/main/res/layout/key_list_public_fragment.xml b/OpenPGP-Keychain/src/main/res/layout/key_list_fragment.xml similarity index 94% rename from OpenPGP-Keychain/src/main/res/layout/key_list_public_fragment.xml rename to OpenPGP-Keychain/src/main/res/layout/key_list_fragment.xml index db82c8771..77bd6f4e9 100644 --- a/OpenPGP-Keychain/src/main/res/layout/key_list_public_fragment.xml +++ b/OpenPGP-Keychain/src/main/res/layout/key_list_fragment.xml @@ -7,7 +7,7 @@ - \ No newline at end of file + 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..09ac1c856 --- /dev/null +++ b/OpenPGP-Keychain/src/main/res/layout/key_list_header.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/src/main/res/layout/key_list_public_item.xml b/OpenPGP-Keychain/src/main/res/layout/key_list_item.xml similarity index 63% rename from OpenPGP-Keychain/src/main/res/layout/key_list_public_item.xml rename to OpenPGP-Keychain/src/main/res/layout/key_list_item.xml index f07d60214..f52693138 100644 --- a/OpenPGP-Keychain/src/main/res/layout/key_list_public_item.xml +++ b/OpenPGP-Keychain/src/main/res/layout/key_list_item.xml @@ -3,13 +3,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="?android:attr/listPreferredItemHeight" - android:layout_marginRight="?android:attr/scrollbarSize" android:gravity="center_vertical" android:paddingLeft="8dp" - android:paddingRight="8dp" android:paddingTop="4dp" android:paddingBottom="4dp" - android:singleLine="true"> + android:singleLine="true" + android:descendantFocusability="blocksDescendants" + android:focusable="false"> @@ -30,6 +31,23 @@ android:layout_alignParentLeft="true" android:layout_alignParentStart="true" /> +