aboutsummaryrefslogtreecommitdiffstats
path: root/OpenPGP-Keychain/src/org/sufficientlysecure
diff options
context:
space:
mode:
authorDominik Schürmann <dominik@dominikschuermann.de>2013-01-16 14:31:16 +0100
committerDominik Schürmann <dominik@dominikschuermann.de>2013-01-16 14:31:16 +0100
commit1feb948acf81532f82b36456080920543004b097 (patch)
treef22e51163db4303ad72b9205a3347aea8211c15e /OpenPGP-Keychain/src/org/sufficientlysecure
parentdbbd8f6856086a9aa17b565080959fb77dc24cd9 (diff)
downloadopen-keychain-1feb948acf81532f82b36456080920543004b097.tar.gz
open-keychain-1feb948acf81532f82b36456080920543004b097.tar.bz2
open-keychain-1feb948acf81532f82b36456080920543004b097.zip
Renaming APG to OpenPGP Keychain
Diffstat (limited to 'OpenPGP-Keychain/src/org/sufficientlysecure')
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Constants.java57
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Id.java231
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/KeychainApplication.java48
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java97
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java66
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java37
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/FileHelper.java118
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/OtherHelper.java167
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpConversionHelper.java140
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpHelper.java407
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpMain.java1857
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/Preferences.java172
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainContract.java220
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainDatabase.java104
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProvider.java852
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderExternal.java31
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderInternal.java29
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobContract.java40
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobDatabase.java47
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobProvider.java151
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/ProviderHelper.java663
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainApiService.aidl146
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainKeyService.aidl32
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainApiService.java323
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentService.java884
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java109
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainKeyService.java138
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/PassphraseCacheService.java230
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainDecryptHandler.aidl27
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainEncryptHandler.aidl28
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetDecryptionKeyIdHandler.aidl25
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetKeyringsHandler.aidl28
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainSignHandler.aidl28
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainVerifyHandler.aidl26
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java946
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java593
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java1105
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpActivity.java175
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java106
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentHtml.java109
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java473
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java148
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java314
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java88
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java121
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java220
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java124
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java146
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java353
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java146
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/MainActivity.java101
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesActivity.java240
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java162
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java164
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java232
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java131
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java154
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareActivity.java74
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareNfcBeamActivity.java245
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java313
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java119
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java137
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java194
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/LookupUnknownKeyDialogFragment.java136
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java275
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java123
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java190
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/DashboardLayout.java186
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/Editor.java25
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ExpandableListFragment.java530
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ImportKeysListLoader.java170
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java95
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java236
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyListAdapter.java264
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java78
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java327
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SelectKeyCursorAdapter.java142
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java196
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Choice.java45
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/HkpKeyServer.java257
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/InputData.java40
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/IterableIterator.java31
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/KeyServer.java97
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Log.java83
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/PositionAwareInputStream.java81
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Primes.java188
-rw-r--r--OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/ProgressDialogUpdater.java25
87 files changed, 18811 insertions, 0 deletions
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Constants.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Constants.java
new file mode 100644
index 000000000..10e431d4b
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Constants.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain;
+
+import android.os.Environment;
+
+public final class Constants {
+
+ public static final boolean DEBUG = true;
+
+ public static final String TAG = "APG";
+
+ public static final String PACKAGE_NAME = "org.sufficientlysecure.keychain";
+
+ public static final String NFC_MIME = "application/org.sufficientlysecure.keychain";
+
+ public static final String PERMISSION_ACCESS_KEY_DATABASE = PACKAGE_NAME
+ + ".permission.ACCESS_KEY_DATABASE";
+ public static final String PERMISSION_ACCESS_API = PACKAGE_NAME + ".permission.ACCESS_API";
+
+ public static final String INTENT_PREFIX = PACKAGE_NAME + ".action.";
+
+ public static final class path {
+ public static final String APP_DIR = Environment.getExternalStorageDirectory() + "/APG";
+ }
+
+ public static final class pref {
+ public static final String DEFAULT_ENCRYPTION_ALGORITHM = "defaultEncryptionAlgorithm";
+ public static final String DEFAULT_HASH_ALGORITHM = "defaultHashAlgorithm";
+ public static final String DEFAULT_ASCII_ARMOUR = "defaultAsciiArmour";
+ public static final String DEFAULT_MESSAGE_COMPRESSION = "defaultMessageCompression";
+ public static final String DEFAULT_FILE_COMPRESSION = "defaultFileCompression";
+ public static final String PASS_PHRASE_CACHE_TTL = "passPhraseCacheTtl";
+ public static final String LANGUAGE = "language";
+ public static final String FORCE_V3_SIGNATURES = "forceV3Signatures";
+ public static final String KEY_SERVERS = "keyServers";
+ }
+
+ public static final class defaults {
+ public static final String KEY_SERVERS = "pool.sks-keyservers.net, subkeys.pgp.net, pgp.mit.edu";
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Id.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Id.java
new file mode 100644
index 000000000..382f144d7
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/Id.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain;
+
+import org.spongycastle.bcpg.CompressionAlgorithmTags;
+
+/**
+ *
+ * TODO:
+ *
+ * - refactor ids, some are not needed and can be done with xml
+ *
+ */
+public final class Id {
+
+ public static final String TAG = "APG";
+
+ public static final class menu {
+ public static final int export = 0x21070001;
+ public static final int delete = 0x21070002;
+ public static final int edit = 0x21070003;
+ public static final int update = 0x21070004;
+ public static final int exportToServer = 0x21070005;
+ public static final int share = 0x21070006;
+ public static final int share_qr_code = 0x21070007;
+ public static final int share_nfc = 0x21070008;
+ public static final int signKey = 0x21070009;
+
+ public static final class option {
+ public static final int new_pass_phrase = 0x21070001;
+ public static final int create = 0x21070002;
+ public static final int about = 0x21070003;
+ public static final int manage_public_keys = 0x21070004;
+ public static final int manage_secret_keys = 0x21070005;
+ public static final int export_keys = 0x21070007;
+ public static final int preferences = 0x21070008;
+ public static final int search = 0x21070009;
+ public static final int help = 0x21070010;
+ public static final int key_server = 0x21070011;
+ public static final int scanQRCode = 0x21070012;
+ public static final int encrypt = 0x21070013;
+ public static final int encrypt_to_clipboard = 0x21070014;
+ public static final int decrypt = 0x21070015;
+ public static final int reply = 0x21070016;
+ public static final int cancel = 0x21070017;
+ public static final int save = 0x21070018;
+ public static final int okay = 0x21070019;
+ public static final int import_from_file = 0x21070020;
+ public static final int import_from_qr_code = 0x21070021;
+ public static final int import_from_nfc = 0x21070022;
+ }
+ }
+
+ // use only lower 16 bits due to compatibility lib
+ public static final class message {
+ public static final int progress_update = 0x00006001;
+ public static final int done = 0x00006002;
+ public static final int import_keys = 0x00006003;
+ public static final int export_keys = 0x00006004;
+ public static final int import_done = 0x00006005;
+ public static final int export_done = 0x00006006;
+ public static final int create_key = 0x00006007;
+ public static final int edit_key = 0x00006008;
+ public static final int delete_done = 0x00006009;
+ public static final int query_done = 0x00006010;
+ public static final int unknown_signature_key = 0x00006011;
+ }
+
+ // public static final class message {
+ // public static final int progress_update = 0x21070001;
+ // public static final int done = 0x21070002;
+ // public static final int import_keys = 0x21070003;
+ // public static final int export_keys = 0x21070004;
+ // public static final int import_done = 0x21070005;
+ // public static final int export_done = 0x21070006;
+ // public static final int create_key = 0x21070007;
+ // public static final int edit_key = 0x21070008;
+ // public static final int delete_done = 0x21070009;
+ // public static final int query_done = 0x21070010;
+ // public static final int unknown_signature_key = 0x21070011;
+ // }
+
+ // use only lower 16 bits due to compatibility lib
+ public static final class request {
+ public static final int public_keys = 0x00007001;
+ public static final int secret_keys = 0x00007002;
+ public static final int filename = 0x00007003;
+ public static final int output_filename = 0x00007004;
+ public static final int key_server_preference = 0x00007005;
+ public static final int look_up_key_id = 0x00007006;
+ public static final int export_to_server = 0x00007007;
+ public static final int import_from_qr_code = 0x00007008;
+ public static final int sign_key = 0x00007009;
+ }
+
+ // public static final class request {
+ // public static final int public_keys = 0x21070001;
+ // public static final int secret_keys = 0x21070002;
+ // public static final int filename = 0x21070003;
+ // public static final int output_filename = 0x21070004;
+ // public static final int key_server_preference = 0x21070005;
+ // public static final int look_up_key_id = 0x21070006;
+ // public static final int export_to_server = 0x21070007;
+ // public static final int import_from_qr_code = 0x21070008;
+ // public static final int sign_key = 0x21070009;
+ // }
+
+ public static final class dialog {
+ public static final int pass_phrase = 0x21070001;
+ public static final int encrypting = 0x21070002;
+ public static final int decrypting = 0x21070003;
+ public static final int new_pass_phrase = 0x21070004;
+ public static final int pass_phrases_do_not_match = 0x21070005;
+ public static final int no_pass_phrase = 0x21070006;
+ public static final int saving = 0x21070007;
+ public static final int delete_key = 0x21070008;
+ public static final int import_keys = 0x21070009;
+ public static final int importing = 0x2107000a;
+ public static final int export_key = 0x2107000b;
+ public static final int export_keys = 0x2107000c;
+ public static final int exporting = 0x2107000d;
+ public static final int new_account = 0x2107000e;
+ // public static final int about = 0x2107000f;
+ public static final int change_log = 0x21070010;
+ public static final int output_filename = 0x21070011;
+ public static final int delete_file = 0x21070012;
+ public static final int deleting = 0x21070013;
+ public static final int help = 0x21070014;
+ public static final int querying = 0x21070015;
+ public static final int lookup_unknown_key = 0x21070016;
+ public static final int signing = 0x21070017;
+ }
+
+ public static final class task {
+ public static final int import_keys = 0x21070001;
+ public static final int export_keys = 0x21070002;
+ }
+
+ // public static final class database {
+ // public static final int type_public = 0;
+ // public static final int type_secret = 1;
+ // }
+
+ public static final class type {
+ public static final int public_key = 0x21070001;
+ public static final int secret_key = 0x21070002;
+ public static final int user_id = 0x21070003;
+ public static final int key = 0x21070004;
+ }
+
+ public static final class choice {
+ public static final class algorithm {
+ public static final int dsa = 0x21070001;
+ public static final int elgamal = 0x21070002;
+ public static final int rsa = 0x21070003;
+ }
+
+ public static final class compression {
+ public static final int none = 0x21070001;
+ public static final int zlib = CompressionAlgorithmTags.ZLIB;
+ public static final int bzip2 = CompressionAlgorithmTags.BZIP2;
+ public static final int zip = CompressionAlgorithmTags.ZIP;
+ }
+
+ public static final class usage {
+ public static final int sign_only = 0x21070001;
+ public static final int encrypt_only = 0x21070002;
+ public static final int sign_and_encrypt = 0x21070003;
+ }
+
+ public static final class action {
+ public static final int encrypt = 0x21070001;
+ public static final int decrypt = 0x21070002;
+ public static final int import_public = 0x21070003;
+ public static final int import_secret = 0x21070004;
+ }
+ }
+
+ public static final class return_value {
+ public static final int ok = 0;
+ public static final int error = -1;
+ public static final int no_master_key = -2;
+ public static final int updated = 1;
+ public static final int bad = -3;
+ }
+
+ public static final class target {
+ public static final int clipboard = 0x21070001;
+ public static final int email = 0x21070002;
+ public static final int file = 0x21070003;
+ public static final int message = 0x21070004;
+ }
+
+ public static final class mode {
+ public static final int undefined = 0x21070001;
+ public static final int byte_array = 0x21070002;
+ public static final int file = 0x21070003;
+ public static final int stream = 0x21070004;
+ }
+
+ public static final class key {
+ public static final int none = 0;
+ public static final int symmetric = -1;
+ }
+
+ public static final class content {
+ public static final int unknown = 0;
+ public static final int encrypted_data = 1;
+ public static final int keys = 2;
+ }
+
+ public static final class keyserver {
+ public static final int search = 0x21070001;
+ public static final int get = 0x21070002;
+ public static final int add = 0x21070003;
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/KeychainApplication.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/KeychainApplication.java
new file mode 100644
index 000000000..dbe9f6394
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/KeychainApplication.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain;
+
+import java.io.File;
+import java.security.Security;
+
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+
+import android.app.Application;
+import android.os.Environment;
+
+public class KeychainApplication extends Application {
+
+ static {
+ // Define Java Security Provider to be Bouncy Castle
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // Create APG directory on sdcard if not existing
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ File dir = new File(Constants.path.APP_DIR);
+ if (!dir.exists() && !dir.mkdirs()) {
+ // ignore this for now, it's not crucial
+ // that the directory doesn't exist at this point
+ }
+ }
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java
new file mode 100644
index 000000000..5ea5bd09a
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ClipboardReflection.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.compatibility;
+
+import java.lang.reflect.Method;
+
+import android.content.Context;
+
+import org.sufficientlysecure.keychain.util.Log;
+
+public class ClipboardReflection {
+
+ private static final String clipboardLabel = "APG";
+
+ /**
+ * Wrapper around ClipboardManager based on Android version using Reflection API
+ *
+ * @param context
+ * @param text
+ */
+ public static void copyToClipboard(Context context, String text) {
+ Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE);
+ try {
+ if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) {
+ Method methodSetText = clipboard.getClass()
+ .getMethod("setText", CharSequence.class);
+ methodSetText.invoke(clipboard, text);
+ } else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) {
+ Class<?> classClipData = Class.forName("android.content.ClipData");
+ Method methodNewPlainText = classClipData.getMethod("newPlainText",
+ CharSequence.class, CharSequence.class);
+ Object clip = methodNewPlainText.invoke(null, clipboardLabel, text);
+ methodNewPlainText = clipboard.getClass()
+ .getMethod("setPrimaryClip", classClipData);
+ methodNewPlainText.invoke(clipboard, clip);
+ }
+ } catch (Exception e) {
+ Log.e("ProjectsException", "There was and error copying the text to the clipboard: "
+ + e.getMessage());
+ }
+ }
+
+ /**
+ * Wrapper around ClipboardManager based on Android version using Reflection API
+ *
+ * @param context
+ * @param text
+ */
+ public static CharSequence getClipboardText(Context context) {
+ Object clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE);
+ try {
+ if ("android.text.ClipboardManager".equals(clipboard.getClass().getName())) {
+ // CharSequence text = clipboard.getText();
+ Method methodGetText = clipboard.getClass().getMethod("getText");
+ Object text = methodGetText.invoke(clipboard);
+
+ return (CharSequence) text;
+ } else if ("android.content.ClipboardManager".equals(clipboard.getClass().getName())) {
+ // ClipData clipData = clipboard.getPrimaryClip();
+ Method methodGetPrimaryClip = clipboard.getClass().getMethod("getPrimaryClip");
+ Object clipData = methodGetPrimaryClip.invoke(clipboard);
+
+ // ClipData.Item clipDataItem = clipData.getItemAt(0);
+ Method methodGetItemAt = clipData.getClass().getMethod("getItemAt", int.class);
+ Object clipDataItem = methodGetItemAt.invoke(clipData, 0);
+
+ // CharSequence text = clipDataItem.coerceToText(context);
+ Method methodGetString = clipDataItem.getClass().getMethod("coerceToText",
+ Context.class);
+ Object text = methodGetString.invoke(clipDataItem, context);
+
+ return (CharSequence) text;
+ } else {
+ return null;
+ }
+ } catch (Exception e) {
+ Log.e("ProjectsException", "There was and error getting the text from the clipboard: "
+ + e.getMessage());
+
+ return null;
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java
new file mode 100644
index 000000000..8c0a26a28
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/DialogFragmentWorkaround.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.compatibility;
+
+import android.os.Build;
+import android.os.Handler;
+
+/**
+ * Bug on Android >= 4.2
+ *
+ * http://code.google.com/p/android/issues/detail?id=41901
+ *
+ * DialogFragment disappears on pressing home and comming back. This also happens especially in
+ * FileDialogFragment after launching a file manager and coming back.
+ *
+ * Usage: <code>
+ * DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() {
+ * public void run() {
+ * // show dialog...
+ * }
+ * });
+ * </code>
+ */
+public class DialogFragmentWorkaround {
+ public static final SDKLevel17Interface INTERFACE = ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) ? new SDKLevel17Impl()
+ : new SDKLevelPriorLevel17Impl());
+
+ private static final int RUNNABLE_DELAY = 300;
+
+ public interface SDKLevel17Interface {
+ // Workaround for http://code.google.com/p/android/issues/detail?id=41901
+ void runnableRunDelayed(Runnable runnable);
+ }
+
+ private static class SDKLevelPriorLevel17Impl implements SDKLevel17Interface {
+ @Override
+ public void runnableRunDelayed(Runnable runnable) {
+ runnable.run();
+ }
+ }
+
+ private static class SDKLevel17Impl implements SDKLevel17Interface {
+ @Override
+ public void runnableRunDelayed(Runnable runnable) {
+ new Handler().postDelayed(runnable, RUNNABLE_DELAY);
+ }
+ }
+
+ // Can't instantiate this class
+ private DialogFragmentWorkaround() {
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java
new file mode 100644
index 000000000..a1f27ca20
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/compatibility/ListFragmentWorkaround.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.compatibility;
+
+import android.view.View;
+import android.widget.ListView;
+
+import com.actionbarsherlock.app.SherlockListFragment;
+
+/**
+ * Bug on Android >= 4.1
+ *
+ * http://code.google.com/p/android/issues/detail?id=35885
+ *
+ * Items are not checked in layout
+ */
+public class ListFragmentWorkaround extends SherlockListFragment {
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ l.setItemChecked(position, l.isItemChecked(position));
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/FileHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/FileHelper.java
new file mode 100644
index 000000000..f16fd874b
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/FileHelper.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.helper;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.util.Log;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.widget.Toast;
+
+public class FileHelper {
+
+ /**
+ * Checks if external storage is mounted if file is located on external storage
+ *
+ * @param file
+ * @return true if storage is mounted
+ */
+ public static boolean isStorageMounted(String file) {
+ if (file.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Opens the preferred installed file manager on Android and shows a toast if no manager is
+ * installed.
+ *
+ * @param activity
+ * @param filename
+ * default selected file, not supported by all file managers
+ * @param type
+ * can be text/plain for example
+ * @param requestCode
+ * requestCode used to identify the result coming back from file manager to
+ * onActivityResult() in your activity
+ */
+ public static void openFile(Activity activity, String filename, String type, int requestCode) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+
+ intent.setData(Uri.parse("file://" + filename));
+ intent.setType(type);
+
+ try {
+ activity.startActivityForResult(intent, requestCode);
+ } catch (ActivityNotFoundException e) {
+ // No compatible file manager was found.
+ Toast.makeText(activity, R.string.noFilemanagerInstalled, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * Get a file path from a Uri.
+ *
+ * from https://github.com/iPaulPro/aFileChooser/blob/master/aFileChooser/src/com/ipaulpro/
+ * afilechooser/utils/FileUtils.java
+ *
+ * @param context
+ * @param uri
+ * @return
+ *
+ * @author paulburke
+ */
+ public static String getPath(Context context, Uri uri) {
+ Log.d(Constants.TAG + " File -",
+ "Authority: " + uri.getAuthority() + ", Fragment: " + uri.getFragment()
+ + ", Port: " + uri.getPort() + ", Query: " + uri.getQuery() + ", Scheme: "
+ + uri.getScheme() + ", Host: " + uri.getHost() + ", Segments: "
+ + uri.getPathSegments().toString());
+
+ if ("content".equalsIgnoreCase(uri.getScheme())) {
+ String[] projection = { "_data" };
+ Cursor cursor = null;
+
+ try {
+ cursor = context.getContentResolver().query(uri, projection, null, null, null);
+ int column_index = cursor.getColumnIndexOrThrow("_data");
+ if (cursor.moveToFirst()) {
+ return cursor.getString(column_index);
+ }
+ } catch (Exception e) {
+ // Eat it
+ }
+ }
+
+ else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ return uri.getPath();
+ }
+
+ return null;
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/OtherHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/OtherHelper.java
new file mode 100644
index 000000000..6a71ca0ba
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/OtherHelper.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.helper;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+import java.util.Set;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+import android.app.Activity;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.widget.Toast;
+
+public class OtherHelper {
+
+ /**
+ * Return the number if days between two dates
+ *
+ * @param first
+ * @param second
+ * @return number of days
+ */
+ public static long getNumDaysBetween(GregorianCalendar first, GregorianCalendar second) {
+ GregorianCalendar tmp = new GregorianCalendar();
+ tmp.setTime(first.getTime());
+ long numDays = (second.getTimeInMillis() - first.getTimeInMillis()) / 1000 / 86400;
+ tmp.add(Calendar.DAY_OF_MONTH, (int) numDays);
+ while (tmp.before(second)) {
+ tmp.add(Calendar.DAY_OF_MONTH, 1);
+ ++numDays;
+ }
+ return numDays;
+ }
+
+ /**
+ * Logs bundle content to debug for inspecting the content
+ *
+ * @param bundle
+ * @param bundleName
+ */
+ public static void logDebugBundle(Bundle bundle, String bundleName) {
+ if (Constants.DEBUG) {
+ if (bundle != null) {
+ Set<String> ks = bundle.keySet();
+ Iterator<String> iterator = ks.iterator();
+
+ Log.d(Constants.TAG, "Bundle " + bundleName + ":");
+ Log.d(Constants.TAG, "------------------------------");
+ while (iterator.hasNext()) {
+ String key = iterator.next();
+ Object value = bundle.get(key);
+
+ if (value != null) {
+ Log.d(Constants.TAG, key + " : " + value.toString());
+ } else {
+ Log.d(Constants.TAG, key + " : null");
+ }
+ }
+ Log.d(Constants.TAG, "------------------------------");
+ } else {
+ Log.d(Constants.TAG, "Bundle " + bundleName + ": null");
+ }
+ }
+ }
+
+ /**
+ * Set actionbar without home button if called from another app
+ *
+ * @param activity
+ */
+ public static void setActionBarBackButton(SherlockFragmentActivity activity) {
+ // set actionbar without home button if called from another app
+ final ActionBar actionBar = activity.getSupportActionBar();
+ Log.d(Constants.TAG, "calling package (only set when using startActivityForResult)="
+ + activity.getCallingPackage());
+ if (activity.getCallingPackage() != null
+ && activity.getCallingPackage().equals(Constants.PACKAGE_NAME)) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+ } else {
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+ }
+ }
+
+ /**
+ * Check if the calling package has the needed permission to invoke an intent with specific
+ * restricted actions.
+ *
+ * If pkgName is null, this will also deny the use of the given action
+ *
+ * @param activity
+ * @param pkgName
+ * @param permName
+ * @param action
+ * @param restrictedActions
+ */
+ public static void checkPackagePermissionForActions(Activity activity, String pkgName,
+ String permName, String action, String[] restrictedActions) {
+ if (action != null) {
+ PackageManager pkgManager = activity.getPackageManager();
+
+ for (int i = 0; i < restrictedActions.length; i++) {
+ if (restrictedActions[i].equals(action)) {
+ if (pkgName != null
+ && (pkgManager.checkPermission(permName, pkgName) == PackageManager.PERMISSION_GRANTED || pkgName
+ .equals(Constants.PACKAGE_NAME))) {
+ Log.d(Constants.TAG, pkgName + " has permission " + permName + ". Action "
+ + action + " was granted!");
+ } else {
+ String error = pkgName + " does NOT have permission " + permName
+ + ". Action " + action + " was NOT granted!";
+ Log.e(Constants.TAG, error);
+ Toast.makeText(activity, activity.getString(R.string.errorMessage, error),
+ Toast.LENGTH_LONG).show();
+
+ // end activity
+ activity.setResult(Activity.RESULT_CANCELED, null);
+ activity.finish();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Splits userId string into naming part and email part
+ *
+ * @param userId
+ * @return array with naming (0) and email (1)
+ */
+ public static String[] splitUserId(String userId) {
+ String[] output = new String[2];
+
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ output[1] = "<" + chunks[1];
+ }
+ output[0] = userId;
+
+ return output;
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpConversionHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpConversionHelper.java
new file mode 100644
index 000000000..3be4c595c
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpConversionHelper.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.helper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPObjectFactory;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPUtil;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+
+
+public class PgpConversionHelper {
+
+ /**
+ * Convert from byte[] to PGPKeyRing
+ *
+ * @param keysBytes
+ * @return
+ */
+ public static PGPKeyRing BytesToPGPKeyRing(byte[] keysBytes) {
+ PGPObjectFactory factory = new PGPObjectFactory(keysBytes);
+ PGPKeyRing keyRing = null;
+ try {
+ if ((keyRing = (PGPKeyRing) factory.nextObject()) == null) {
+ Log.e(Constants.TAG, "No keys given!");
+ }
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while converting to PGPKeyRing!", e);
+ }
+
+ return keyRing;
+ }
+
+ /**
+ * Convert from byte[] to ArrayList<PGPSecretKey>
+ *
+ * @param keysBytes
+ * @return
+ */
+ public static ArrayList<PGPSecretKey> BytesToPGPSecretKeyList(byte[] keysBytes) {
+ PGPSecretKeyRing keyRing = (PGPSecretKeyRing) BytesToPGPKeyRing(keysBytes);
+ ArrayList<PGPSecretKey> keys = new ArrayList<PGPSecretKey>();
+
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSecretKey> itr = keyRing.getSecretKeys();
+ while (itr.hasNext()) {
+ keys.add(itr.next());
+ }
+
+ return keys;
+ }
+
+ /**
+ * Convert from byte[] to PGPSecretKey
+ *
+ * Singles keys are encoded as keyRings with one single key in it by Bouncy Castle
+ *
+ * @param keysBytes
+ * @return
+ */
+ public static PGPSecretKey BytesToPGPSecretKey(byte[] keyBytes) {
+ PGPSecretKey key = BytesToPGPSecretKeyList(keyBytes).get(0);
+
+ return key;
+ }
+
+ /**
+ * Convert from ArrayList<PGPSecretKey> to byte[]
+ *
+ * @param keys
+ * @return
+ */
+ public static byte[] PGPSecretKeyArrayListToBytes(ArrayList<PGPSecretKey> keys) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ for (PGPSecretKey key : keys) {
+ try {
+ key.encode(os);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while converting ArrayList<PGPSecretKey> to byte[]!", e);
+ }
+ }
+
+ return os.toByteArray();
+ }
+
+ /**
+ * Convert from PGPSecretKey to byte[]
+ *
+ * @param keysBytes
+ * @return
+ */
+ public static byte[] PGPSecretKeyToBytes(PGPSecretKey key) {
+ try {
+ return key.getEncoded();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Encoding failed", e);
+
+ return null;
+ }
+ }
+
+ /**
+ * Convert from PGPSecretKeyRing to byte[]
+ *
+ * @param keysBytes
+ * @return
+ */
+ public static byte[] PGPSecretKeyRingToBytes(PGPSecretKeyRing keyRing) {
+ try {
+ return keyRing.getEncoded();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Encoding failed", e);
+
+ return null;
+ }
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpHelper.java
new file mode 100644
index 000000000..f59bd58de
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpHelper.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.helper;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.Vector;
+
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.IterableIterator;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Context;
+
+public class PgpHelper {
+
+ public static Date getCreationDate(PGPPublicKey key) {
+ return key.getCreationTime();
+ }
+
+ public static Date getCreationDate(PGPSecretKey key) {
+ return key.getPublicKey().getCreationTime();
+ }
+
+ @SuppressWarnings("unchecked")
+ public static PGPPublicKey getMasterKey(PGPPublicKeyRing keyRing) {
+ if (keyRing == null) {
+ return null;
+ }
+ for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
+ if (key.isMasterKey()) {
+ return key;
+ }
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static PGPSecretKey getMasterKey(PGPSecretKeyRing keyRing) {
+ if (keyRing == null) {
+ return null;
+ }
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ if (key.isMasterKey()) {
+ return key;
+ }
+ }
+
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Vector<PGPPublicKey> getEncryptKeys(PGPPublicKeyRing keyRing) {
+ Vector<PGPPublicKey> encryptKeys = new Vector<PGPPublicKey>();
+
+ for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
+ if (isEncryptionKey(key)) {
+ encryptKeys.add(key);
+ }
+ }
+
+ return encryptKeys;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Vector<PGPSecretKey> getSigningKeys(PGPSecretKeyRing keyRing) {
+ Vector<PGPSecretKey> signingKeys = new Vector<PGPSecretKey>();
+
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ if (isSigningKey(key)) {
+ signingKeys.add(key);
+ }
+ }
+
+ return signingKeys;
+ }
+
+ public static Vector<PGPPublicKey> getUsableEncryptKeys(PGPPublicKeyRing keyRing) {
+ Vector<PGPPublicKey> usableKeys = new Vector<PGPPublicKey>();
+ Vector<PGPPublicKey> encryptKeys = getEncryptKeys(keyRing);
+ PGPPublicKey masterKey = null;
+ for (int i = 0; i < encryptKeys.size(); ++i) {
+ PGPPublicKey key = encryptKeys.get(i);
+ if (!isExpired(key)) {
+ if (key.isMasterKey()) {
+ masterKey = key;
+ } else {
+ usableKeys.add(key);
+ }
+ }
+ }
+ if (masterKey != null) {
+ usableKeys.add(masterKey);
+ }
+ return usableKeys;
+ }
+
+ public static boolean isExpired(PGPPublicKey key) {
+ Date creationDate = getCreationDate(key);
+ Date expiryDate = getExpiryDate(key);
+ Date now = new Date();
+ if (now.compareTo(creationDate) >= 0
+ && (expiryDate == null || now.compareTo(expiryDate) <= 0)) {
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean isExpired(PGPSecretKey key) {
+ return isExpired(key.getPublicKey());
+ }
+
+ public static Vector<PGPSecretKey> getUsableSigningKeys(PGPSecretKeyRing keyRing) {
+ Vector<PGPSecretKey> usableKeys = new Vector<PGPSecretKey>();
+ Vector<PGPSecretKey> signingKeys = getSigningKeys(keyRing);
+ PGPSecretKey masterKey = null;
+ for (int i = 0; i < signingKeys.size(); ++i) {
+ PGPSecretKey key = signingKeys.get(i);
+ if (key.isMasterKey()) {
+ masterKey = key;
+ } else {
+ usableKeys.add(key);
+ }
+ }
+ if (masterKey != null) {
+ usableKeys.add(masterKey);
+ }
+ return usableKeys;
+ }
+
+ public static Date getExpiryDate(PGPPublicKey key) {
+ Date creationDate = getCreationDate(key);
+ if (key.getValidDays() == 0) {
+ // no expiry
+ return null;
+ }
+ Calendar calendar = GregorianCalendar.getInstance();
+ calendar.setTime(creationDate);
+ calendar.add(Calendar.DATE, key.getValidDays());
+ Date expiryDate = calendar.getTime();
+
+ return expiryDate;
+ }
+
+ public static Date getExpiryDate(PGPSecretKey key) {
+ return getExpiryDate(key.getPublicKey());
+ }
+
+ public static PGPPublicKey getEncryptPublicKey(Context context, long masterKeyId) {
+ PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(context,
+ masterKeyId);
+ if (keyRing == null) {
+ Log.e(Constants.TAG, "keyRing is null!");
+ return null;
+ }
+ Vector<PGPPublicKey> encryptKeys = getUsableEncryptKeys(keyRing);
+ if (encryptKeys.size() == 0) {
+ Log.e(Constants.TAG, "encryptKeys is null!");
+ return null;
+ }
+ return encryptKeys.get(0);
+ }
+
+ public static PGPSecretKey getSigningKey(Context context, long masterKeyId) {
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(context,
+ masterKeyId);
+ if (keyRing == null) {
+ return null;
+ }
+ Vector<PGPSecretKey> signingKeys = getUsableSigningKeys(keyRing);
+ if (signingKeys.size() == 0) {
+ return null;
+ }
+ return signingKeys.get(0);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static String getMainUserId(PGPPublicKey key) {
+ for (String userId : new IterableIterator<String>(key.getUserIDs())) {
+ return userId;
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static String getMainUserId(PGPSecretKey key) {
+ for (String userId : new IterableIterator<String>(key.getUserIDs())) {
+ return userId;
+ }
+ return null;
+ }
+
+ public static String getMainUserIdSafe(Context context, PGPPublicKey key) {
+ String userId = getMainUserId(key);
+ if (userId == null || userId.equals("")) {
+ userId = context.getString(R.string.unknownUserId);
+ }
+ return userId;
+ }
+
+ public static String getMainUserIdSafe(Context context, PGPSecretKey key) {
+ String userId = getMainUserId(key);
+ if (userId == null || userId.equals("")) {
+ userId = context.getString(R.string.unknownUserId);
+ }
+ return userId;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static boolean isEncryptionKey(PGPPublicKey key) {
+ if (!key.isEncryptionKey()) {
+ return false;
+ }
+
+ if (key.getVersion() <= 3) {
+ // this must be true now
+ return key.isEncryptionKey();
+ }
+
+ // special cases
+ if (key.getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT) {
+ return true;
+ }
+
+ if (key.getAlgorithm() == PGPPublicKey.RSA_ENCRYPT) {
+ return true;
+ }
+
+ for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
+ if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
+ continue;
+ }
+ PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
+
+ if (hashed != null
+ && (hashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) {
+ return true;
+ }
+
+ PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
+
+ if (unhashed != null
+ && (unhashed.getKeyFlags() & (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean isEncryptionKey(PGPSecretKey key) {
+ return isEncryptionKey(key.getPublicKey());
+ }
+
+ @SuppressWarnings("unchecked")
+ public static boolean isSigningKey(PGPPublicKey key) {
+ if (key.getVersion() <= 3) {
+ return true;
+ }
+
+ // special case
+ if (key.getAlgorithm() == PGPPublicKey.RSA_SIGN) {
+ return true;
+ }
+
+ for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
+ if (key.isMasterKey() && sig.getKeyID() != key.getKeyID()) {
+ continue;
+ }
+ PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
+
+ if (hashed != null && (hashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) {
+ return true;
+ }
+
+ PGPSignatureSubpacketVector unhashed = sig.getUnhashedSubPackets();
+
+ if (unhashed != null && (unhashed.getKeyFlags() & KeyFlags.SIGN_DATA) != 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static boolean isSigningKey(PGPSecretKey key) {
+ return isSigningKey(key.getPublicKey());
+ }
+
+ public static String getAlgorithmInfo(PGPPublicKey key) {
+ return getAlgorithmInfo(key.getAlgorithm(), key.getBitStrength());
+ }
+
+ public static String getAlgorithmInfo(PGPSecretKey key) {
+ return getAlgorithmInfo(key.getPublicKey());
+ }
+
+ public static String getAlgorithmInfo(int algorithm, int keySize) {
+ String algorithmStr = null;
+
+ switch (algorithm) {
+ case PGPPublicKey.RSA_ENCRYPT:
+ case PGPPublicKey.RSA_GENERAL:
+ case PGPPublicKey.RSA_SIGN: {
+ algorithmStr = "RSA";
+ break;
+ }
+
+ case PGPPublicKey.DSA: {
+ algorithmStr = "DSA";
+ break;
+ }
+
+ case PGPPublicKey.ELGAMAL_ENCRYPT:
+ case PGPPublicKey.ELGAMAL_GENERAL: {
+ algorithmStr = "ElGamal";
+ break;
+ }
+
+ default: {
+ algorithmStr = "???";
+ break;
+ }
+ }
+ return algorithmStr + ", " + keySize + "bit";
+ }
+
+ public static String convertFingerprintToHex(byte[] fp) {
+ String fingerPrint = "";
+ for (int i = 0; i < fp.length; ++i) {
+ if (i != 0 && i % 10 == 0) {
+ fingerPrint += " ";
+ } else if (i != 0 && i % 2 == 0) {
+ fingerPrint += " ";
+ }
+ String chunk = Integer.toHexString((fp[i] + 256) % 256).toUpperCase(Locale.US);
+ while (chunk.length() < 2) {
+ chunk = "0" + chunk;
+ }
+ fingerPrint += chunk;
+ }
+
+ return fingerPrint;
+
+ }
+
+ public static String getFingerPrint(Context context, long keyId) {
+ PGPPublicKey key = ProviderHelper.getPGPPublicKeyByKeyId(context, keyId);
+ // if it is no public key get it from your own keys...
+ if (key == null) {
+ PGPSecretKey secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, keyId);
+ if (secretKey == null) {
+ Log.e(Constants.TAG, "Key could not be found!");
+ return null;
+ }
+ key = secretKey.getPublicKey();
+ }
+
+ return convertFingerprintToHex(key.getFingerprint());
+ }
+
+ public static String getSmallFingerPrint(long keyId) {
+ String fingerPrint = Long.toHexString(keyId & 0xffffffffL).toUpperCase(Locale.US);
+ while (fingerPrint.length() < 8) {
+ fingerPrint = "0" + fingerPrint;
+ }
+ return fingerPrint;
+ }
+
+ public static String keyToHex(long keyId) {
+ return getSmallFingerPrint(keyId >> 32) + getSmallFingerPrint(keyId);
+ }
+
+ public static long keyFromHex(String data) {
+ int len = data.length();
+ String s2 = data.substring(len - 8);
+ String s1 = data.substring(0, len - 8);
+ return (Long.parseLong(s1, 16) << 32) | Long.parseLong(s2, 16);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpMain.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpMain.java
new file mode 100644
index 000000000..26842e4dc
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/PgpMain.java
@@ -0,0 +1,1857 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.helper;
+
+import org.spongycastle.bcpg.ArmoredInputStream;
+import org.spongycastle.bcpg.ArmoredOutputStream;
+import org.spongycastle.bcpg.BCPGOutputStream;
+import org.spongycastle.bcpg.CompressionAlgorithmTags;
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.bcpg.SymmetricKeyAlgorithmTags;
+import org.spongycastle.bcpg.sig.KeyFlags;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.jce.spec.ElGamalParameterSpec;
+import org.spongycastle.openpgp.PGPCompressedData;
+import org.spongycastle.openpgp.PGPCompressedDataGenerator;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.spongycastle.openpgp.PGPEncryptedDataGenerator;
+import org.spongycastle.openpgp.PGPEncryptedDataList;
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPKeyPair;
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPKeyRingGenerator;
+import org.spongycastle.openpgp.PGPLiteralData;
+import org.spongycastle.openpgp.PGPLiteralDataGenerator;
+import org.spongycastle.openpgp.PGPObjectFactory;
+import org.spongycastle.openpgp.PGPOnePassSignature;
+import org.spongycastle.openpgp.PGPOnePassSignatureList;
+import org.spongycastle.openpgp.PGPPBEEncryptedData;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyEncryptedData;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPPublicKeyRingCollection;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.spongycastle.openpgp.PGPSignatureGenerator;
+import org.spongycastle.openpgp.PGPSignatureList;
+import org.spongycastle.openpgp.PGPSignatureSubpacketGenerator;
+import org.spongycastle.openpgp.PGPSignatureSubpacketVector;
+import org.spongycastle.openpgp.PGPUtil;
+import org.spongycastle.openpgp.PGPV3SignatureGenerator;
+import org.spongycastle.openpgp.operator.PBEDataDecryptorFactory;
+import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.spongycastle.openpgp.operator.PBESecretKeyEncryptor;
+import org.spongycastle.openpgp.operator.PGPContentSignerBuilder;
+import org.spongycastle.openpgp.operator.PGPDigestCalculator;
+import org.spongycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.spongycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
+import org.spongycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator;
+import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder;
+import org.spongycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.util.HkpKeyServer;
+import org.sufficientlysecure.keychain.util.InputData;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.PositionAwareInputStream;
+import org.sufficientlysecure.keychain.util.Primes;
+import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
+import org.sufficientlysecure.keychain.util.KeyServer.AddKeyException;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.Environment;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.math.BigInteger;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.SignatureException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.regex.Pattern;
+
+/**
+ * TODO:
+ *
+ * - Separate this file into different helpers
+ *
+ */
+public class PgpMain {
+
+ static {
+ // register spongy castle provider
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ // Not BC due to the use of Spongy Castle for Android
+ public static final String BOUNCY_CASTLE_PROVIDER_NAME = "SC";
+
+ private static final int[] PREFERRED_SYMMETRIC_ALGORITHMS = new int[] {
+ SymmetricKeyAlgorithmTags.AES_256, SymmetricKeyAlgorithmTags.AES_192,
+ SymmetricKeyAlgorithmTags.AES_128, SymmetricKeyAlgorithmTags.CAST5,
+ SymmetricKeyAlgorithmTags.TRIPLE_DES };
+ private static final int[] PREFERRED_HASH_ALGORITHMS = new int[] { HashAlgorithmTags.SHA1,
+ HashAlgorithmTags.SHA256, HashAlgorithmTags.RIPEMD160 };
+ private static final int[] PREFERRED_COMPRESSION_ALGORITHMS = new int[] {
+ CompressionAlgorithmTags.ZLIB, CompressionAlgorithmTags.BZIP2,
+ CompressionAlgorithmTags.ZIP };
+
+ public static Pattern PGP_MESSAGE = Pattern.compile(
+ ".*?(-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----).*", Pattern.DOTALL);
+
+ public static Pattern PGP_SIGNED_MESSAGE = Pattern
+ .compile(
+ ".*?(-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----).*",
+ Pattern.DOTALL);
+
+ public static Pattern PGP_PUBLIC_KEY = Pattern.compile(
+ ".*?(-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----).*",
+ Pattern.DOTALL);
+
+ private static String mEditPassPhrase = null;
+
+ public static class PgpGeneralException extends Exception {
+ static final long serialVersionUID = 0xf812773342L;
+
+ public PgpGeneralException(String message) {
+ super(message);
+ }
+ }
+
+ public static class NoAsymmetricEncryptionException extends Exception {
+ static final long serialVersionUID = 0xf812773343L;
+
+ public NoAsymmetricEncryptionException() {
+ super();
+ }
+ }
+
+ public static void setEditPassPhrase(String passPhrase) {
+ mEditPassPhrase = passPhrase;
+ }
+
+ public static String getEditPassPhrase() {
+ return mEditPassPhrase;
+ }
+
+ public static void updateProgress(ProgressDialogUpdater progress, int message, int current,
+ int total) {
+ if (progress != null) {
+ progress.setProgress(message, current, total);
+ }
+ }
+
+ public static void updateProgress(ProgressDialogUpdater progress, int current, int total) {
+ if (progress != null) {
+ progress.setProgress(current, total);
+ }
+ }
+
+ /**
+ * Creates new secret key. The returned PGPSecretKeyRing contains only one newly generated key
+ * when this key is the new masterkey. If a masterkey is supplied in the parameters
+ * PGPSecretKeyRing contains the masterkey and the new key as a subkey (certified by the
+ * masterkey).
+ *
+ * @param context
+ * @param algorithmChoice
+ * @param keySize
+ * @param passPhrase
+ * @param masterSecretKey
+ * @return
+ * @throws NoSuchAlgorithmException
+ * @throws PGPException
+ * @throws NoSuchProviderException
+ * @throws PgpGeneralException
+ * @throws InvalidAlgorithmParameterException
+ */
+ public static PGPSecretKeyRing createKey(Context context, int algorithmChoice, int keySize,
+ String passPhrase, PGPSecretKey masterSecretKey) throws NoSuchAlgorithmException,
+ PGPException, NoSuchProviderException, PgpGeneralException,
+ InvalidAlgorithmParameterException {
+
+ if (keySize < 512) {
+ throw new PgpGeneralException(context.getString(R.string.error_keySizeMinimum512bit));
+ }
+
+ if (passPhrase == null) {
+ passPhrase = "";
+ }
+
+ int algorithm = 0;
+ KeyPairGenerator keyGen = null;
+
+ switch (algorithmChoice) {
+ case Id.choice.algorithm.dsa: {
+ keyGen = KeyPairGenerator.getInstance("DSA", BOUNCY_CASTLE_PROVIDER_NAME);
+ keyGen.initialize(keySize, new SecureRandom());
+ algorithm = PGPPublicKey.DSA;
+ break;
+ }
+
+ case Id.choice.algorithm.elgamal: {
+ if (masterSecretKey == null) {
+ throw new PgpGeneralException(
+ context.getString(R.string.error_masterKeyMustNotBeElGamal));
+ }
+ keyGen = KeyPairGenerator.getInstance("ElGamal", BOUNCY_CASTLE_PROVIDER_NAME);
+ BigInteger p = Primes.getBestPrime(keySize);
+ BigInteger g = new BigInteger("2");
+
+ ElGamalParameterSpec elParams = new ElGamalParameterSpec(p, g);
+
+ keyGen.initialize(elParams);
+ algorithm = PGPPublicKey.ELGAMAL_ENCRYPT;
+ break;
+ }
+
+ case Id.choice.algorithm.rsa: {
+ keyGen = KeyPairGenerator.getInstance("RSA", BOUNCY_CASTLE_PROVIDER_NAME);
+ keyGen.initialize(keySize, new SecureRandom());
+
+ algorithm = PGPPublicKey.RSA_GENERAL;
+ break;
+ }
+
+ default: {
+ throw new PgpGeneralException(context.getString(R.string.error_unknownAlgorithmChoice));
+ }
+ }
+
+ // build new key pair
+ PGPKeyPair keyPair = new JcaPGPKeyPair(algorithm, keyGen.generateKeyPair(), new Date());
+
+ // define hashing and signing algos
+ PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(
+ HashAlgorithmTags.SHA1);
+
+ // Build key encrypter and decrypter based on passphrase
+ PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
+ PGPEncryptedData.CAST5, sha1Calc).setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(
+ passPhrase.toCharArray());
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ BOUNCY_CASTLE_PROVIDER_NAME).build(passPhrase.toCharArray());
+
+ PGPKeyRingGenerator ringGen = null;
+ PGPContentSignerBuilder certificationSignerBuilder = null;
+ if (masterSecretKey == null) {
+ certificationSignerBuilder = new JcaPGPContentSignerBuilder(keyPair.getPublicKey()
+ .getAlgorithm(), HashAlgorithmTags.SHA1);
+
+ // build keyRing with only this one master key in it!
+ ringGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, keyPair, "",
+ sha1Calc, null, null, certificationSignerBuilder, keyEncryptor);
+ } else {
+ PGPPublicKey masterPublicKey = masterSecretKey.getPublicKey();
+ PGPPrivateKey masterPrivateKey = masterSecretKey.extractPrivateKey(keyDecryptor);
+ PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
+
+ certificationSignerBuilder = new JcaPGPContentSignerBuilder(masterKeyPair
+ .getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1);
+
+ // build keyRing with master key and new key as subkey (certified by masterkey)
+ ringGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION, masterKeyPair,
+ "", sha1Calc, null, null, certificationSignerBuilder, keyEncryptor);
+
+ ringGen.addSubKey(keyPair);
+ }
+
+ PGPSecretKeyRing secKeyRing = ringGen.generateSecretKeyRing();
+
+ return secKeyRing;
+ }
+
+ public static void buildSecretKey(Context context, ArrayList<String> userIds,
+ ArrayList<PGPSecretKey> keys, ArrayList<Integer> keysUsages, long masterKeyId,
+ String oldPassPhrase, String newPassPhrase, ProgressDialogUpdater progress)
+ throws PgpGeneralException, NoSuchProviderException, PGPException,
+ NoSuchAlgorithmException, SignatureException, IOException {
+
+ Log.d(Constants.TAG, "userIds: " + userIds.toString());
+
+ updateProgress(progress, R.string.progress_buildingKey, 0, 100);
+
+ if (oldPassPhrase == null) {
+ oldPassPhrase = "";
+ }
+ if (newPassPhrase == null) {
+ newPassPhrase = "";
+ }
+
+ updateProgress(progress, R.string.progress_preparingMasterKey, 10, 100);
+
+ int usageId = keysUsages.get(0);
+ boolean canSign = (usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt);
+ boolean canEncrypt = (usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt);
+
+ String mainUserId = userIds.get(0);
+
+ PGPSecretKey masterKey = keys.get(0);
+
+ // this removes all userIds and certifications previously attached to the masterPublicKey
+ PGPPublicKey tmpKey = masterKey.getPublicKey();
+ PGPPublicKey masterPublicKey = new PGPPublicKey(tmpKey.getAlgorithm(),
+ tmpKey.getKey(new BouncyCastleProvider()), tmpKey.getCreationTime());
+
+ // already done by code above:
+ // PGPPublicKey masterPublicKey = masterKey.getPublicKey();
+ // // Somehow, the PGPPublicKey already has an empty certification attached to it when the
+ // // keyRing is generated the first time, we remove that when it exists, before adding the
+ // new
+ // // ones
+ // PGPPublicKey masterPublicKeyRmCert = PGPPublicKey.removeCertification(masterPublicKey,
+ // "");
+ // if (masterPublicKeyRmCert != null) {
+ // masterPublicKey = masterPublicKeyRmCert;
+ // }
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassPhrase.toCharArray());
+ PGPPrivateKey masterPrivateKey = masterKey.extractPrivateKey(keyDecryptor);
+
+ updateProgress(progress, R.string.progress_certifyingMasterKey, 20, 100);
+
+ for (String userId : userIds) {
+ PGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
+ masterPublicKey.getAlgorithm(), HashAlgorithmTags.SHA1)
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME);
+ PGPSignatureGenerator sGen = new PGPSignatureGenerator(signerBuilder);
+
+ sGen.init(PGPSignature.POSITIVE_CERTIFICATION, masterPrivateKey);
+
+ PGPSignature certification = sGen.generateCertification(userId, masterPublicKey);
+
+ masterPublicKey = PGPPublicKey.addCertification(masterPublicKey, userId, certification);
+ }
+
+ // TODO: cross-certify the master key with every sub key (APG 1)
+
+ PGPKeyPair masterKeyPair = new PGPKeyPair(masterPublicKey, masterPrivateKey);
+
+ PGPSignatureSubpacketGenerator hashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ PGPSignatureSubpacketGenerator unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
+
+ int keyFlags = KeyFlags.CERTIFY_OTHER | KeyFlags.SIGN_DATA;
+ if (canEncrypt) {
+ keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
+ }
+ hashedPacketsGen.setKeyFlags(true, keyFlags);
+
+ hashedPacketsGen.setPreferredSymmetricAlgorithms(true, PREFERRED_SYMMETRIC_ALGORITHMS);
+ hashedPacketsGen.setPreferredHashAlgorithms(true, PREFERRED_HASH_ALGORITHMS);
+ hashedPacketsGen.setPreferredCompressionAlgorithms(true, PREFERRED_COMPRESSION_ALGORITHMS);
+
+ // TODO: this doesn't work quite right yet (APG 1)
+ // if (keyEditor.getExpiryDate() != null) {
+ // GregorianCalendar creationDate = new GregorianCalendar();
+ // creationDate.setTime(getCreationDate(masterKey));
+ // GregorianCalendar expiryDate = keyEditor.getExpiryDate();
+ // long numDays = Utils.getNumDaysBetween(creationDate, expiryDate);
+ // if (numDays <= 0) {
+ // throw new GeneralException(
+ // context.getString(R.string.error_expiryMustComeAfterCreation));
+ // }
+ // hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400);
+ // }
+
+ updateProgress(progress, R.string.progress_buildingMasterKeyRing, 30, 100);
+
+ // define hashing and signing algos
+ PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(
+ HashAlgorithmTags.SHA1);
+ PGPContentSignerBuilder certificationSignerBuilder = new JcaPGPContentSignerBuilder(
+ masterKeyPair.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1);
+
+ // Build key encrypter based on passphrase
+ PBESecretKeyEncryptor keyEncryptor = new JcePBESecretKeyEncryptorBuilder(
+ PGPEncryptedData.CAST5, sha1Calc).setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(
+ newPassPhrase.toCharArray());
+
+ PGPKeyRingGenerator keyGen = new PGPKeyRingGenerator(PGPSignature.POSITIVE_CERTIFICATION,
+ masterKeyPair, mainUserId, sha1Calc, hashedPacketsGen.generate(),
+ unhashedPacketsGen.generate(), certificationSignerBuilder, keyEncryptor);
+
+ updateProgress(progress, R.string.progress_addingSubKeys, 40, 100);
+
+ for (int i = 1; i < keys.size(); ++i) {
+ updateProgress(progress, 40 + 50 * (i - 1) / (keys.size() - 1), 100);
+
+ PGPSecretKey subKey = keys.get(i);
+ PGPPublicKey subPublicKey = subKey.getPublicKey();
+
+ PBESecretKeyDecryptor keyDecryptor2 = new JcePBESecretKeyDecryptorBuilder()
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(oldPassPhrase.toCharArray());
+ PGPPrivateKey subPrivateKey = subKey.extractPrivateKey(keyDecryptor2);
+
+ // TODO: now used without algorithm and creation time?! (APG 1)
+ PGPKeyPair subKeyPair = new PGPKeyPair(subPublicKey, subPrivateKey);
+
+ hashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
+
+ keyFlags = 0;
+
+ usageId = keysUsages.get(i);
+ canSign = (usageId == Id.choice.usage.sign_only || usageId == Id.choice.usage.sign_and_encrypt);
+ canEncrypt = (usageId == Id.choice.usage.encrypt_only || usageId == Id.choice.usage.sign_and_encrypt);
+ if (canSign) {
+ keyFlags |= KeyFlags.SIGN_DATA;
+ }
+ if (canEncrypt) {
+ keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
+ }
+ hashedPacketsGen.setKeyFlags(true, keyFlags);
+
+ // TODO: this doesn't work quite right yet (APG 1)
+ // if (keyEditor.getExpiryDate() != null) {
+ // GregorianCalendar creationDate = new GregorianCalendar();
+ // creationDate.setTime(getCreationDate(masterKey));
+ // GregorianCalendar expiryDate = keyEditor.getExpiryDate();
+ // long numDays = Utils.getNumDaysBetween(creationDate, expiryDate);
+ // if (numDays <= 0) {
+ // throw new GeneralException(
+ // context.getString(R.string.error_expiryMustComeAfterCreation));
+ // }
+ // hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400);
+ // }
+
+ keyGen.addSubKey(subKeyPair, hashedPacketsGen.generate(), unhashedPacketsGen.generate());
+ }
+
+ PGPSecretKeyRing secretKeyRing = keyGen.generateSecretKeyRing();
+ PGPPublicKeyRing publicKeyRing = keyGen.generatePublicKeyRing();
+
+ updateProgress(progress, R.string.progress_savingKeyRing, 90, 100);
+
+ ProviderHelper.saveKeyRing(context, secretKeyRing);
+ ProviderHelper.saveKeyRing(context, publicKeyRing);
+
+ updateProgress(progress, R.string.progress_done, 100, 100);
+ }
+
+ /**
+ * TODO: implement Id.return_value.updated as status when key already existed
+ *
+ * @param context
+ * @param keyring
+ * @return
+ */
+ public static int storeKeyRingInCache(Context context, PGPKeyRing keyring) {
+ int status = Integer.MIN_VALUE; // out of bounds value (Id.return_value.*)
+ try {
+ if (keyring instanceof PGPSecretKeyRing) {
+ PGPSecretKeyRing secretKeyRing = (PGPSecretKeyRing) keyring;
+ boolean save = true;
+ try {
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(new char[] {});
+ PGPPrivateKey testKey = secretKeyRing.getSecretKey().extractPrivateKey(
+ keyDecryptor);
+ if (testKey == null) {
+ // this is bad, something is very wrong... likely a --export-secret-subkeys
+ // export
+ save = false;
+ status = Id.return_value.bad;
+ }
+ } catch (PGPException e) {
+ // all good if this fails, we likely didn't use the right password
+ }
+
+ if (save) {
+ ProviderHelper.saveKeyRing(context, secretKeyRing);
+ // TODO: remove status returns, use exceptions!
+ status = Id.return_value.ok;
+ }
+ } else if (keyring instanceof PGPPublicKeyRing) {
+ PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring;
+ ProviderHelper.saveKeyRing(context, publicKeyRing);
+ // TODO: remove status returns, use exceptions!
+ status = Id.return_value.ok;
+ }
+ } catch (IOException e) {
+ status = Id.return_value.error;
+ }
+
+ return status;
+ }
+
+ public static boolean uploadKeyRingToServer(HkpKeyServer server, PGPPublicKeyRing keyring) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ArmoredOutputStream aos = new ArmoredOutputStream(bos);
+ try {
+ aos.write(keyring.getEncoded());
+ aos.close();
+
+ String armouredKey = bos.toString("UTF-8");
+ server.add(armouredKey);
+
+ return true;
+ } catch (IOException e) {
+ return false;
+ } catch (AddKeyException e) {
+ // TODO: tell the user?
+ return false;
+ } finally {
+ try {
+ bos.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ public static Bundle importKeyRings(Context context, InputData data,
+ ProgressDialogUpdater progress) throws PgpGeneralException, FileNotFoundException,
+ PGPException, IOException {
+ Bundle returnData = new Bundle();
+
+ updateProgress(progress, R.string.progress_importingSecretKeys, 0, 100);
+
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ throw new PgpGeneralException(context.getString(R.string.error_externalStorageNotReady));
+ }
+
+ PositionAwareInputStream progressIn = new PositionAwareInputStream(data.getInputStream());
+
+ // need to have access to the bufferedInput, so we can reuse it for the possible
+ // PGPObject chunks after the first one, e.g. files with several consecutive ASCII
+ // armour blocks
+ BufferedInputStream bufferedInput = new BufferedInputStream(progressIn);
+ int newKeys = 0;
+ int oldKeys = 0;
+ int badKeys = 0;
+ try {
+
+ // read all available blocks... (asc files can contain many blocks with BEGIN END)
+ while (bufferedInput.available() > 0) {
+ InputStream in = PGPUtil.getDecoderStream(bufferedInput);
+ PGPObjectFactory objectFactory = new PGPObjectFactory(in);
+
+ // go through all objects in this block
+ Object obj;
+ while ((obj = objectFactory.nextObject()) != null) {
+ Log.d(Constants.TAG, "Found class: " + obj.getClass());
+
+ if (obj instanceof PGPKeyRing) {
+ PGPKeyRing keyring = (PGPKeyRing) obj;
+
+ int status = Integer.MIN_VALUE; // out of bounds value
+
+ status = storeKeyRingInCache(context, keyring);
+
+ if (status == Id.return_value.error) {
+ throw new PgpGeneralException(
+ context.getString(R.string.error_savingKeys));
+ }
+
+ // update the counts to display to the user at the end
+ if (status == Id.return_value.updated) {
+ ++oldKeys;
+ } else if (status == Id.return_value.ok) {
+ ++newKeys;
+ } else if (status == Id.return_value.bad) {
+ ++badKeys;
+ }
+
+ updateProgress(progress,
+ (int) (100 * progressIn.position() / data.getSize()), 100);
+ } else {
+ Log.e(Constants.TAG, "Object not recognized as PGPKeyRing!");
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Exception on parsing key file!", e);
+ }
+
+ returnData.putInt(KeychainIntentService.RESULT_IMPORT_ADDED, newKeys);
+ returnData.putInt(KeychainIntentService.RESULT_IMPORT_UPDATED, oldKeys);
+ returnData.putInt(KeychainIntentService.RESULT_IMPORT_BAD, badKeys);
+
+ updateProgress(progress, R.string.progress_done, 100, 100);
+
+ return returnData;
+ }
+
+ public static Bundle exportKeyRings(Context context, ArrayList<Long> keyRingMasterKeyIds,
+ int keyType, OutputStream outStream, ProgressDialogUpdater progress)
+ throws PgpGeneralException, FileNotFoundException, PGPException, IOException {
+ Bundle returnData = new Bundle();
+
+ if (keyRingMasterKeyIds.size() == 1) {
+ updateProgress(progress, R.string.progress_exportingKey, 0, 100);
+ } else {
+ updateProgress(progress, R.string.progress_exportingKeys, 0, 100);
+ }
+
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ throw new PgpGeneralException(context.getString(R.string.error_externalStorageNotReady));
+ }
+
+ // export public keyrings...
+ ArmoredOutputStream outPub = new ArmoredOutputStream(outStream);
+ outPub.setHeader("Version", getFullVersion(context));
+
+ int numKeys = 0;
+ for (int i = 0; i < keyRingMasterKeyIds.size(); ++i) {
+ // double the needed time if exporting both public and secret parts
+ if (keyType == Id.type.secret_key) {
+ updateProgress(progress, i * 100 / keyRingMasterKeyIds.size() / 2, 100);
+ } else {
+ updateProgress(progress, i * 100 / keyRingMasterKeyIds.size(), 100);
+ }
+
+ PGPPublicKeyRing publicKeyRing = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(
+ context, keyRingMasterKeyIds.get(i));
+
+ if (publicKeyRing != null) {
+ publicKeyRing.encode(outPub);
+ }
+ ++numKeys;
+ }
+ outPub.close();
+
+ // if we export secret keyrings, append all secret parts after the public parts
+ if (keyType == Id.type.secret_key) {
+ ArmoredOutputStream outSec = new ArmoredOutputStream(outStream);
+ outSec.setHeader("Version", getFullVersion(context));
+
+ for (int i = 0; i < keyRingMasterKeyIds.size(); ++i) {
+ updateProgress(progress, i * 100 / keyRingMasterKeyIds.size() / 2, 100);
+
+ PGPSecretKeyRing secretKeyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(
+ context, keyRingMasterKeyIds.get(i));
+
+ if (secretKeyRing != null) {
+ secretKeyRing.encode(outSec);
+ }
+ }
+ outSec.close();
+ }
+
+ returnData.putInt(KeychainIntentService.RESULT_EXPORT, numKeys);
+
+ updateProgress(progress, R.string.progress_done, 100, 100);
+
+ return returnData;
+ }
+
+ /**
+ * Encrypt and Sign data
+ *
+ * @param context
+ * @param progress
+ * @param data
+ * @param outStream
+ * @param useAsciiArmor
+ * @param compression
+ * @param encryptionKeyIds
+ * @param symmetricEncryptionAlgorithm
+ * @param encryptionPassphrase
+ * @param signatureKeyId
+ * @param signatureHashAlgorithm
+ * @param signatureForceV3
+ * @param signaturePassphrase
+ * @throws IOException
+ * @throws PgpGeneralException
+ * @throws PGPException
+ * @throws NoSuchProviderException
+ * @throws NoSuchAlgorithmException
+ * @throws SignatureException
+ */
+ public static void encryptAndSign(Context context, ProgressDialogUpdater progress,
+ InputData data, OutputStream outStream, boolean useAsciiArmor, int compression,
+ long encryptionKeyIds[], String encryptionPassphrase, int symmetricEncryptionAlgorithm,
+ long signatureKeyId, int signatureHashAlgorithm, boolean signatureForceV3,
+ String signaturePassphrase) throws IOException, PgpGeneralException, PGPException,
+ NoSuchProviderException, NoSuchAlgorithmException, SignatureException {
+
+ if (encryptionKeyIds == null) {
+ encryptionKeyIds = new long[0];
+ }
+
+ ArmoredOutputStream armorOut = null;
+ OutputStream out = null;
+ OutputStream encryptOut = null;
+ if (useAsciiArmor) {
+ armorOut = new ArmoredOutputStream(outStream);
+ armorOut.setHeader("Version", getFullVersion(context));
+ out = armorOut;
+ } else {
+ out = outStream;
+ }
+ PGPSecretKey signingKey = null;
+ PGPSecretKeyRing signingKeyRing = null;
+ PGPPrivateKey signaturePrivateKey = null;
+
+ if (encryptionKeyIds.length == 0 && encryptionPassphrase == null) {
+ throw new PgpGeneralException(
+ context.getString(R.string.error_noEncryptionKeysOrPassPhrase));
+ }
+
+ if (signatureKeyId != Id.key.none) {
+ signingKeyRing = ProviderHelper.getPGPSecretKeyRingByKeyId(context, signatureKeyId);
+ signingKey = PgpHelper.getSigningKey(context, signatureKeyId);
+ if (signingKey == null) {
+ throw new PgpGeneralException(context.getString(R.string.error_signatureFailed));
+ }
+
+ if (signaturePassphrase == null) {
+ throw new PgpGeneralException(
+ context.getString(R.string.error_noSignaturePassPhrase));
+ }
+
+ updateProgress(progress, R.string.progress_extractingSignatureKey, 0, 100);
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ BOUNCY_CASTLE_PROVIDER_NAME).build(signaturePassphrase.toCharArray());
+ signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor);
+ if (signaturePrivateKey == null) {
+ throw new PgpGeneralException(
+ context.getString(R.string.error_couldNotExtractPrivateKey));
+ }
+ }
+ updateProgress(progress, R.string.progress_preparingStreams, 5, 100);
+
+ // encrypt and compress input file content
+ JcePGPDataEncryptorBuilder encryptorBuilder = new JcePGPDataEncryptorBuilder(
+ symmetricEncryptionAlgorithm).setProvider(BOUNCY_CASTLE_PROVIDER_NAME)
+ .setWithIntegrityPacket(true);
+
+ PGPEncryptedDataGenerator cPk = new PGPEncryptedDataGenerator(encryptorBuilder);
+
+ if (encryptionKeyIds.length == 0) {
+ // Symmetric encryption
+ Log.d(Constants.TAG, "encryptionKeyIds length is 0 -> symmetric encryption");
+
+ JcePBEKeyEncryptionMethodGenerator symmetricEncryptionGenerator = new JcePBEKeyEncryptionMethodGenerator(
+ encryptionPassphrase.toCharArray());
+ cPk.addMethod(symmetricEncryptionGenerator);
+ } else {
+ // Asymmetric encryption
+ for (int i = 0; i < encryptionKeyIds.length; ++i) {
+ PGPPublicKey key = PgpHelper.getEncryptPublicKey(context, encryptionKeyIds[i]);
+ if (key != null) {
+
+ JcePublicKeyKeyEncryptionMethodGenerator pubKeyEncryptionGenerator = new JcePublicKeyKeyEncryptionMethodGenerator(
+ key);
+ cPk.addMethod(pubKeyEncryptionGenerator);
+ }
+ }
+ }
+ encryptOut = cPk.open(out, new byte[1 << 16]);
+
+ PGPSignatureGenerator signatureGenerator = null;
+ PGPV3SignatureGenerator signatureV3Generator = null;
+
+ if (signatureKeyId != Id.key.none) {
+ updateProgress(progress, R.string.progress_preparingSignature, 10, 100);
+
+ // content signer based on signing key algorithm and choosen hash algorithm
+ JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(
+ signingKey.getPublicKey().getAlgorithm(), signatureHashAlgorithm)
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME);
+
+ if (signatureForceV3) {
+ signatureV3Generator = new PGPV3SignatureGenerator(contentSignerBuilder);
+ signatureV3Generator.init(PGPSignature.BINARY_DOCUMENT, signaturePrivateKey);
+ } else {
+ signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder);
+ signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, signaturePrivateKey);
+
+ String userId = PgpHelper.getMainUserId(PgpHelper.getMasterKey(signingKeyRing));
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+ spGen.setSignerUserID(false, userId);
+ signatureGenerator.setHashedSubpackets(spGen.generate());
+ }
+ }
+
+ PGPCompressedDataGenerator compressGen = null;
+ BCPGOutputStream bcpgOut = null;
+ if (compression == Id.choice.compression.none) {
+ bcpgOut = new BCPGOutputStream(encryptOut);
+ } else {
+ compressGen = new PGPCompressedDataGenerator(compression);
+ bcpgOut = new BCPGOutputStream(compressGen.open(encryptOut));
+ }
+ if (signatureKeyId != Id.key.none) {
+ if (signatureForceV3) {
+ signatureV3Generator.generateOnePassVersion(false).encode(bcpgOut);
+ } else {
+ signatureGenerator.generateOnePassVersion(false).encode(bcpgOut);
+ }
+ }
+
+ PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator();
+ // file name not needed, so empty string
+ OutputStream pOut = literalGen.open(bcpgOut, PGPLiteralData.BINARY, "", new Date(),
+ new byte[1 << 16]);
+ updateProgress(progress, R.string.progress_encrypting, 20, 100);
+
+ long done = 0;
+ int n = 0;
+ byte[] buffer = new byte[1 << 16];
+ InputStream in = data.getInputStream();
+ while ((n = in.read(buffer)) > 0) {
+ pOut.write(buffer, 0, n);
+ if (signatureKeyId != Id.key.none) {
+ if (signatureForceV3) {
+ signatureV3Generator.update(buffer, 0, n);
+ } else {
+ signatureGenerator.update(buffer, 0, n);
+ }
+ }
+ done += n;
+ if (data.getSize() != 0) {
+ updateProgress(progress, (int) (20 + (95 - 20) * done / data.getSize()), 100);
+ }
+ }
+
+ literalGen.close();
+
+ if (signatureKeyId != Id.key.none) {
+ updateProgress(progress, R.string.progress_generatingSignature, 95, 100);
+ if (signatureForceV3) {
+ signatureV3Generator.generate().encode(pOut);
+ } else {
+ signatureGenerator.generate().encode(pOut);
+ }
+ }
+ if (compressGen != null) {
+ compressGen.close();
+ }
+ encryptOut.close();
+ if (useAsciiArmor) {
+ armorOut.close();
+ }
+
+ updateProgress(progress, R.string.progress_done, 100, 100);
+ }
+
+ public static void signText(Context context, ProgressDialogUpdater progress, InputData data,
+ OutputStream outStream, long signatureKeyId, String signaturePassphrase,
+ int signatureHashAlgorithm, boolean forceV3Signature) throws PgpGeneralException,
+ PGPException, IOException, NoSuchAlgorithmException, SignatureException {
+
+ ArmoredOutputStream armorOut = new ArmoredOutputStream(outStream);
+ armorOut.setHeader("Version", getFullVersion(context));
+
+ PGPSecretKey signingKey = null;
+ PGPSecretKeyRing signingKeyRing = null;
+ PGPPrivateKey signaturePrivateKey = null;
+
+ if (signatureKeyId == 0) {
+ armorOut.close();
+ throw new PgpGeneralException(context.getString(R.string.error_noSignatureKey));
+ }
+
+ signingKeyRing = ProviderHelper.getPGPSecretKeyRingByKeyId(context, signatureKeyId);
+ signingKey = PgpHelper.getSigningKey(context, signatureKeyId);
+ if (signingKey == null) {
+ armorOut.close();
+ throw new PgpGeneralException(context.getString(R.string.error_signatureFailed));
+ }
+
+ if (signaturePassphrase == null) {
+ armorOut.close();
+ throw new PgpGeneralException(context.getString(R.string.error_noSignaturePassPhrase));
+ }
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ BOUNCY_CASTLE_PROVIDER_NAME).build(signaturePassphrase.toCharArray());
+ signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor);
+ if (signaturePrivateKey == null) {
+ armorOut.close();
+ throw new PgpGeneralException(
+ context.getString(R.string.error_couldNotExtractPrivateKey));
+ }
+ updateProgress(progress, R.string.progress_preparingStreams, 0, 100);
+
+ updateProgress(progress, R.string.progress_preparingSignature, 30, 100);
+
+ PGPSignatureGenerator signatureGenerator = null;
+ PGPV3SignatureGenerator signatureV3Generator = null;
+
+ // content signer based on signing key algorithm and choosen hash algorithm
+ JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(signingKey
+ .getPublicKey().getAlgorithm(), signatureHashAlgorithm)
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME);
+
+ if (forceV3Signature) {
+ signatureV3Generator = new PGPV3SignatureGenerator(contentSignerBuilder);
+ signatureV3Generator.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, signaturePrivateKey);
+ } else {
+ signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder);
+ signatureGenerator.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, signaturePrivateKey);
+
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+ String userId = PgpHelper.getMainUserId(PgpHelper.getMasterKey(signingKeyRing));
+ spGen.setSignerUserID(false, userId);
+ signatureGenerator.setHashedSubpackets(spGen.generate());
+ }
+
+ updateProgress(progress, R.string.progress_signing, 40, 100);
+
+ armorOut.beginClearText(signatureHashAlgorithm);
+
+ InputStream inStream = data.getInputStream();
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(inStream));
+
+ final byte[] newline = "\r\n".getBytes("UTF-8");
+
+ if (forceV3Signature) {
+ processLine(reader.readLine(), armorOut, signatureV3Generator);
+ } else {
+ processLine(reader.readLine(), armorOut, signatureGenerator);
+ }
+
+ while (true) {
+ final String line = reader.readLine();
+
+ if (line == null) {
+ armorOut.write(newline);
+ break;
+ }
+
+ armorOut.write(newline);
+ if (forceV3Signature) {
+ signatureV3Generator.update(newline);
+ processLine(line, armorOut, signatureV3Generator);
+ } else {
+ signatureGenerator.update(newline);
+ processLine(line, armorOut, signatureGenerator);
+ }
+ }
+
+ armorOut.endClearText();
+
+ BCPGOutputStream bOut = new BCPGOutputStream(armorOut);
+ if (forceV3Signature) {
+ signatureV3Generator.generate().encode(bOut);
+ } else {
+ signatureGenerator.generate().encode(bOut);
+ }
+ armorOut.close();
+
+ updateProgress(progress, R.string.progress_done, 100, 100);
+ }
+
+ public static void generateSignature(Context context, ProgressDialogUpdater progress,
+ InputData data, OutputStream outStream, boolean armored, boolean binary,
+ long signatureKeyId, String signaturePassPhrase, int hashAlgorithm,
+ boolean forceV3Signature) throws PgpGeneralException, PGPException, IOException,
+ NoSuchAlgorithmException, SignatureException {
+
+ OutputStream out = null;
+
+ // Ascii Armor (Base64)
+ ArmoredOutputStream armorOut = null;
+ if (armored) {
+ armorOut = new ArmoredOutputStream(outStream);
+ armorOut.setHeader("Version", getFullVersion(context));
+ out = armorOut;
+ } else {
+ out = outStream;
+ }
+
+ PGPSecretKey signingKey = null;
+ PGPSecretKeyRing signingKeyRing = null;
+ PGPPrivateKey signaturePrivateKey = null;
+
+ if (signatureKeyId == 0) {
+ throw new PgpGeneralException(context.getString(R.string.error_noSignatureKey));
+ }
+
+ signingKeyRing = ProviderHelper.getPGPSecretKeyRingByKeyId(context, signatureKeyId);
+ signingKey = PgpHelper.getSigningKey(context, signatureKeyId);
+ if (signingKey == null) {
+ throw new PgpGeneralException(context.getString(R.string.error_signatureFailed));
+ }
+
+ if (signaturePassPhrase == null) {
+ throw new PgpGeneralException(context.getString(R.string.error_noSignaturePassPhrase));
+ }
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ BOUNCY_CASTLE_PROVIDER_NAME).build(signaturePassPhrase.toCharArray());
+ signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor);
+ if (signaturePrivateKey == null) {
+ throw new PgpGeneralException(
+ context.getString(R.string.error_couldNotExtractPrivateKey));
+ }
+ updateProgress(progress, R.string.progress_preparingStreams, 0, 100);
+
+ updateProgress(progress, R.string.progress_preparingSignature, 30, 100);
+
+ PGPSignatureGenerator signatureGenerator = null;
+ PGPV3SignatureGenerator signatureV3Generator = null;
+
+ int type = PGPSignature.CANONICAL_TEXT_DOCUMENT;
+ if (binary) {
+ type = PGPSignature.BINARY_DOCUMENT;
+ }
+
+ // content signer based on signing key algorithm and choosen hash algorithm
+ JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(signingKey
+ .getPublicKey().getAlgorithm(), hashAlgorithm)
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME);
+
+ if (forceV3Signature) {
+ signatureV3Generator = new PGPV3SignatureGenerator(contentSignerBuilder);
+ signatureV3Generator.init(type, signaturePrivateKey);
+ } else {
+ signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder);
+ signatureGenerator.init(type, signaturePrivateKey);
+
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+ String userId = PgpHelper.getMainUserId(PgpHelper.getMasterKey(signingKeyRing));
+ spGen.setSignerUserID(false, userId);
+ signatureGenerator.setHashedSubpackets(spGen.generate());
+ }
+
+ updateProgress(progress, R.string.progress_signing, 40, 100);
+
+ InputStream inStream = data.getInputStream();
+ if (binary) {
+ byte[] buffer = new byte[1 << 16];
+ int n = 0;
+ while ((n = inStream.read(buffer)) > 0) {
+ if (forceV3Signature) {
+ signatureV3Generator.update(buffer, 0, n);
+ } else {
+ signatureGenerator.update(buffer, 0, n);
+ }
+ }
+ } else {
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(inStream));
+ final byte[] newline = "\r\n".getBytes("UTF-8");
+
+ while (true) {
+ final String line = reader.readLine();
+
+ if (line == null) {
+ break;
+ }
+
+ if (forceV3Signature) {
+ processLine(line, null, signatureV3Generator);
+ signatureV3Generator.update(newline);
+ } else {
+ processLine(line, null, signatureGenerator);
+ signatureGenerator.update(newline);
+ }
+ }
+ }
+
+ BCPGOutputStream bOut = new BCPGOutputStream(out);
+ if (forceV3Signature) {
+ signatureV3Generator.generate().encode(bOut);
+ } else {
+ signatureGenerator.generate().encode(bOut);
+ }
+ out.close();
+ outStream.close();
+
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 100, 100);
+ }
+
+ public static PGPPublicKeyRing signKey(Context context, long masterKeyId, long pubKeyId,
+ String passphrase) throws PgpGeneralException, NoSuchAlgorithmException,
+ NoSuchProviderException, PGPException, SignatureException {
+ if (passphrase == null || passphrase.length() <= 0) {
+ throw new PgpGeneralException("Unable to obtain passphrase");
+ } else {
+ PGPPublicKeyRing pubring = ProviderHelper.getPGPPublicKeyRingByKeyId(context, pubKeyId);
+
+ PGPSecretKey signingKey = PgpHelper.getSigningKey(context, masterKeyId);
+ if (signingKey == null) {
+ throw new PgpGeneralException(context.getString(R.string.error_signatureFailed));
+ }
+
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
+ PGPPrivateKey signaturePrivateKey = signingKey.extractPrivateKey(keyDecryptor);
+ if (signaturePrivateKey == null) {
+ throw new PgpGeneralException(
+ context.getString(R.string.error_couldNotExtractPrivateKey));
+ }
+
+ // TODO: SHA256 fixed?
+ JcaPGPContentSignerBuilder contentSignerBuilder = new JcaPGPContentSignerBuilder(
+ signingKey.getPublicKey().getAlgorithm(), PGPUtil.SHA256)
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME);
+
+ PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
+ contentSignerBuilder);
+
+ signatureGenerator.init(PGPSignature.DIRECT_KEY, signaturePrivateKey);
+
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+
+ PGPSignatureSubpacketVector packetVector = spGen.generate();
+ signatureGenerator.setHashedSubpackets(packetVector);
+
+ PGPPublicKey signedKey = PGPPublicKey.addCertification(pubring.getPublicKey(pubKeyId),
+ signatureGenerator.generate());
+ pubring = PGPPublicKeyRing.insertPublicKey(pubring, signedKey);
+
+ return pubring;
+ }
+ }
+
+ public static long getDecryptionKeyId(Context context, InputStream inputStream)
+ throws PgpGeneralException, NoAsymmetricEncryptionException, IOException {
+ InputStream in = PGPUtil.getDecoderStream(inputStream);
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ PGPEncryptedDataList enc;
+ Object o = pgpF.nextObject();
+
+ // the first object might be a PGP marker packet.
+ if (o instanceof PGPEncryptedDataList) {
+ enc = (PGPEncryptedDataList) o;
+ } else {
+ enc = (PGPEncryptedDataList) pgpF.nextObject();
+ }
+
+ if (enc == null) {
+ throw new PgpGeneralException(context.getString(R.string.error_invalidData));
+ }
+
+ // TODO: currently we always only look at the first known key
+ // find the secret key
+ PGPSecretKey secretKey = null;
+ Iterator<?> it = enc.getEncryptedDataObjects();
+ boolean gotAsymmetricEncryption = false;
+ while (it.hasNext()) {
+ Object obj = it.next();
+ if (obj instanceof PGPPublicKeyEncryptedData) {
+ gotAsymmetricEncryption = true;
+ PGPPublicKeyEncryptedData pbe = (PGPPublicKeyEncryptedData) obj;
+ secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, pbe.getKeyID());
+ if (secretKey != null) {
+ break;
+ }
+ }
+ }
+
+ if (!gotAsymmetricEncryption) {
+ throw new NoAsymmetricEncryptionException();
+ }
+
+ if (secretKey == null) {
+ return Id.key.none;
+ }
+
+ return secretKey.getKeyID();
+ }
+
+ public static boolean hasSymmetricEncryption(Context context, InputStream inputStream)
+ throws PgpGeneralException, IOException {
+ InputStream in = PGPUtil.getDecoderStream(inputStream);
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ PGPEncryptedDataList enc;
+ Object o = pgpF.nextObject();
+
+ // the first object might be a PGP marker packet.
+ if (o instanceof PGPEncryptedDataList) {
+ enc = (PGPEncryptedDataList) o;
+ } else {
+ enc = (PGPEncryptedDataList) pgpF.nextObject();
+ }
+
+ if (enc == null) {
+ throw new PgpGeneralException(context.getString(R.string.error_invalidData));
+ }
+
+ Iterator<?> it = enc.getEncryptedDataObjects();
+ while (it.hasNext()) {
+ Object obj = it.next();
+ if (obj instanceof PGPPBEEncryptedData) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static Bundle decryptAndVerify(Context context, ProgressDialogUpdater progress,
+ InputData data, OutputStream outStream, String passphrase, boolean assumeSymmetric)
+ throws IOException, PgpGeneralException, PGPException, SignatureException {
+ if (passphrase == null) {
+ passphrase = "";
+ }
+
+ Bundle returnData = new Bundle();
+ InputStream in = PGPUtil.getDecoderStream(data.getInputStream());
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ PGPEncryptedDataList enc;
+ Object o = pgpF.nextObject();
+ long signatureKeyId = 0;
+
+ int currentProgress = 0;
+ if (progress != null)
+ progress.setProgress(R.string.progress_readingData, currentProgress, 100);
+
+ if (o instanceof PGPEncryptedDataList) {
+ enc = (PGPEncryptedDataList) o;
+ } else {
+ enc = (PGPEncryptedDataList) pgpF.nextObject();
+ }
+
+ if (enc == null) {
+ throw new PgpGeneralException(context.getString(R.string.error_invalidData));
+ }
+
+ InputStream clear = null;
+ PGPEncryptedData encryptedData = null;
+
+ currentProgress += 5;
+
+ // TODO: currently we always only look at the first known key or symmetric encryption,
+ // there might be more...
+ if (assumeSymmetric) {
+ PGPPBEEncryptedData pbe = null;
+ Iterator<?> it = enc.getEncryptedDataObjects();
+ // find secret key
+ while (it.hasNext()) {
+ Object obj = it.next();
+ if (obj instanceof PGPPBEEncryptedData) {
+ pbe = (PGPPBEEncryptedData) obj;
+ break;
+ }
+ }
+
+ if (pbe == null) {
+ throw new PgpGeneralException(
+ context.getString(R.string.error_noSymmetricEncryptionPacket));
+ }
+
+ updateProgress(progress, R.string.progress_preparingStreams, currentProgress, 100);
+
+ PGPDigestCalculatorProvider digestCalcProvider = new JcaPGPDigestCalculatorProviderBuilder()
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build();
+ PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder(
+ digestCalcProvider).setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(
+ passphrase.toCharArray());
+
+ clear = pbe.getDataStream(decryptorFactory);
+
+ encryptedData = pbe;
+ currentProgress += 5;
+ } else {
+ if (progress != null)
+ progress.setProgress(R.string.progress_findingKey, currentProgress, 100);
+ PGPPublicKeyEncryptedData pbe = null;
+ PGPSecretKey secretKey = null;
+ Iterator<?> it = enc.getEncryptedDataObjects();
+ // find secret key
+ while (it.hasNext()) {
+ Object obj = it.next();
+ if (obj instanceof PGPPublicKeyEncryptedData) {
+ PGPPublicKeyEncryptedData encData = (PGPPublicKeyEncryptedData) obj;
+ secretKey = ProviderHelper.getPGPSecretKeyByKeyId(context, encData.getKeyID());
+ if (secretKey != null) {
+ pbe = encData;
+ break;
+ }
+ }
+ }
+
+ if (secretKey == null) {
+ throw new PgpGeneralException(context.getString(R.string.error_noSecretKeyFound));
+ }
+
+ currentProgress += 5;
+ updateProgress(progress, R.string.progress_extractingKey, currentProgress, 100);
+ PGPPrivateKey privateKey = null;
+ try {
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(passphrase.toCharArray());
+ privateKey = secretKey.extractPrivateKey(keyDecryptor);
+ } catch (PGPException e) {
+ throw new PGPException(context.getString(R.string.error_wrongPassPhrase));
+ }
+ if (privateKey == null) {
+ throw new PgpGeneralException(
+ context.getString(R.string.error_couldNotExtractPrivateKey));
+ }
+ currentProgress += 5;
+ updateProgress(progress, R.string.progress_preparingStreams, currentProgress, 100);
+
+ PublicKeyDataDecryptorFactory decryptorFactory = new JcePublicKeyDataDecryptorFactoryBuilder()
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME).build(privateKey);
+
+ clear = pbe.getDataStream(decryptorFactory);
+
+ encryptedData = pbe;
+ currentProgress += 5;
+ }
+
+ PGPObjectFactory plainFact = new PGPObjectFactory(clear);
+ Object dataChunk = plainFact.nextObject();
+ PGPOnePassSignature signature = null;
+ PGPPublicKey signatureKey = null;
+ int signatureIndex = -1;
+
+ if (dataChunk instanceof PGPCompressedData) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_decompressingData, currentProgress, 100);
+ PGPObjectFactory fact = new PGPObjectFactory(
+ ((PGPCompressedData) dataChunk).getDataStream());
+ dataChunk = fact.nextObject();
+ plainFact = fact;
+ currentProgress += 10;
+ }
+
+ if (dataChunk instanceof PGPOnePassSignatureList) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_processingSignature, currentProgress, 100);
+ returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE, true);
+ PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) dataChunk;
+ for (int i = 0; i < sigList.size(); ++i) {
+ signature = sigList.get(i);
+ signatureKey = ProviderHelper.getPGPPublicKeyByKeyId(context, signature.getKeyID());
+ if (signatureKeyId == 0) {
+ signatureKeyId = signature.getKeyID();
+ }
+ if (signatureKey == null) {
+ signature = null;
+ } else {
+ signatureIndex = i;
+ signatureKeyId = signature.getKeyID();
+ String userId = null;
+ PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingByKeyId(
+ context, signatureKeyId);
+ if (signKeyRing != null) {
+ userId = PgpHelper.getMainUserId(PgpHelper.getMasterKey(signKeyRing));
+ }
+ returnData.putString(KeychainIntentService.RESULT_SIGNATURE_USER_ID, userId);
+ break;
+ }
+ }
+
+ returnData.putLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID, signatureKeyId);
+
+ if (signature != null) {
+ JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = new JcaPGPContentVerifierBuilderProvider()
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME);
+
+ signature.init(contentVerifierBuilderProvider, signatureKey);
+ } else {
+ returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN, true);
+ }
+
+ dataChunk = plainFact.nextObject();
+ currentProgress += 10;
+ }
+
+ if (dataChunk instanceof PGPSignatureList) {
+ dataChunk = plainFact.nextObject();
+ }
+
+ if (dataChunk instanceof PGPLiteralData) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_decrypting, currentProgress, 100);
+ PGPLiteralData literalData = (PGPLiteralData) dataChunk;
+ OutputStream out = outStream;
+
+ byte[] buffer = new byte[1 << 16];
+ InputStream dataIn = literalData.getInputStream();
+
+ int startProgress = currentProgress;
+ int endProgress = 100;
+ if (signature != null) {
+ endProgress = 90;
+ } else if (encryptedData.isIntegrityProtected()) {
+ endProgress = 95;
+ }
+ int n = 0;
+ int done = 0;
+ long startPos = data.getStreamPosition();
+ while ((n = dataIn.read(buffer)) > 0) {
+ out.write(buffer, 0, n);
+ done += n;
+ if (signature != null) {
+ try {
+ signature.update(buffer, 0, n);
+ } catch (SignatureException e) {
+ returnData
+ .putBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS, false);
+ signature = null;
+ }
+ }
+ // unknown size, but try to at least have a moving, slowing down progress bar
+ currentProgress = startProgress + (endProgress - startProgress) * done
+ / (done + 100000);
+ if (data.getSize() - startPos == 0) {
+ currentProgress = endProgress;
+ } else {
+ currentProgress = (int) (startProgress + (endProgress - startProgress)
+ * (data.getStreamPosition() - startPos) / (data.getSize() - startPos));
+ }
+ updateProgress(progress, currentProgress, 100);
+ }
+
+ if (signature != null) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_verifyingSignature, 90, 100);
+ PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject();
+ PGPSignature messageSignature = signatureList.get(signatureIndex);
+ if (signature.verify(messageSignature)) {
+ returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS, true);
+ } else {
+ returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS, false);
+ }
+ }
+ }
+
+ // TODO: add integrity somewhere
+ if (encryptedData.isIntegrityProtected()) {
+ if (progress != null)
+ progress.setProgress(R.string.progress_verifyingIntegrity, 95, 100);
+ if (encryptedData.verify()) {
+ // passed
+ } else {
+ // failed
+ }
+ } else {
+ // no integrity check
+ }
+
+ updateProgress(progress, R.string.progress_done, 100, 100);
+ return returnData;
+ }
+
+ public static Bundle verifyText(Context context, ProgressDialogUpdater progress,
+ InputData data, OutputStream outStream, boolean lookupUnknownKey) throws IOException,
+ PgpGeneralException, PGPException, SignatureException {
+ Bundle returnData = new Bundle();
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ArmoredInputStream aIn = new ArmoredInputStream(data.getInputStream());
+
+ updateProgress(progress, R.string.progress_done, 0, 100);
+
+ // mostly taken from ClearSignedFileProcessor
+ ByteArrayOutputStream lineOut = new ByteArrayOutputStream();
+ int lookAhead = readInputLine(lineOut, aIn);
+ byte[] lineSep = getLineSeparator();
+
+ byte[] line = lineOut.toByteArray();
+ out.write(line, 0, getLengthWithoutSeparator(line));
+ out.write(lineSep);
+
+ while (lookAhead != -1 && aIn.isClearText()) {
+ lookAhead = readInputLine(lineOut, lookAhead, aIn);
+ line = lineOut.toByteArray();
+ out.write(line, 0, getLengthWithoutSeparator(line));
+ out.write(lineSep);
+ }
+
+ out.close();
+
+ byte[] clearText = out.toByteArray();
+ outStream.write(clearText);
+
+ returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE, true);
+
+ updateProgress(progress, R.string.progress_processingSignature, 60, 100);
+ PGPObjectFactory pgpFact = new PGPObjectFactory(aIn);
+
+ PGPSignatureList sigList = (PGPSignatureList) pgpFact.nextObject();
+ if (sigList == null) {
+ throw new PgpGeneralException(context.getString(R.string.error_corruptData));
+ }
+ PGPSignature signature = null;
+ long signatureKeyId = 0;
+ PGPPublicKey signatureKey = null;
+ for (int i = 0; i < sigList.size(); ++i) {
+ signature = sigList.get(i);
+ signatureKey = ProviderHelper.getPGPPublicKeyByKeyId(context, signature.getKeyID());
+ if (signatureKeyId == 0) {
+ signatureKeyId = signature.getKeyID();
+ }
+ // if key is not known and we want to lookup unknown ones...
+ if (signatureKey == null && lookupUnknownKey) {
+
+ returnData = new Bundle();
+ returnData.putLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID, signatureKeyId);
+ returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_LOOKUP_KEY, true);
+
+ // return directly now, decrypt will be done again after importing unknown key
+ return returnData;
+ }
+
+ if (signatureKey == null) {
+ signature = null;
+ } else {
+ signatureKeyId = signature.getKeyID();
+ String userId = null;
+ PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingByKeyId(context,
+ signatureKeyId);
+ if (signKeyRing != null) {
+ userId = PgpHelper.getMainUserId(PgpHelper.getMasterKey(signKeyRing));
+ }
+ returnData.putString(KeychainIntentService.RESULT_SIGNATURE_USER_ID, userId);
+ break;
+ }
+ }
+
+ returnData.putLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID, signatureKeyId);
+
+ if (signature == null) {
+ returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN, true);
+ if (progress != null)
+ progress.setProgress(R.string.progress_done, 100, 100);
+ return returnData;
+ }
+
+ JcaPGPContentVerifierBuilderProvider contentVerifierBuilderProvider = new JcaPGPContentVerifierBuilderProvider()
+ .setProvider(BOUNCY_CASTLE_PROVIDER_NAME);
+
+ signature.init(contentVerifierBuilderProvider, signatureKey);
+
+ InputStream sigIn = new BufferedInputStream(new ByteArrayInputStream(clearText));
+
+ lookAhead = readInputLine(lineOut, sigIn);
+
+ processLine(signature, lineOut.toByteArray());
+
+ if (lookAhead != -1) {
+ do {
+ lookAhead = readInputLine(lineOut, lookAhead, sigIn);
+
+ signature.update((byte) '\r');
+ signature.update((byte) '\n');
+
+ processLine(signature, lineOut.toByteArray());
+ } while (lookAhead != -1);
+ }
+
+ returnData.putBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS, signature.verify());
+
+ updateProgress(progress, R.string.progress_done, 100, 100);
+ return returnData;
+ }
+
+ public static int getStreamContent(Context context, InputStream inStream) throws IOException {
+ InputStream in = PGPUtil.getDecoderStream(inStream);
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ Object object = pgpF.nextObject();
+ while (object != null) {
+ if (object instanceof PGPPublicKeyRing || object instanceof PGPSecretKeyRing) {
+ return Id.content.keys;
+ } else if (object instanceof PGPEncryptedDataList) {
+ return Id.content.encrypted_data;
+ }
+ object = pgpF.nextObject();
+ }
+
+ return Id.content.unknown;
+ }
+
+ private static void processLine(final String pLine, final ArmoredOutputStream pArmoredOutput,
+ final PGPSignatureGenerator pSignatureGenerator) throws IOException, SignatureException {
+
+ if (pLine == null) {
+ return;
+ }
+
+ final char[] chars = pLine.toCharArray();
+ int len = chars.length;
+
+ while (len > 0) {
+ if (!Character.isWhitespace(chars[len - 1])) {
+ break;
+ }
+ len--;
+ }
+
+ final byte[] data = pLine.substring(0, len).getBytes("UTF-8");
+
+ if (pArmoredOutput != null) {
+ pArmoredOutput.write(data);
+ }
+ pSignatureGenerator.update(data);
+ }
+
+ private static void processLine(final String pLine, final ArmoredOutputStream pArmoredOutput,
+ final PGPV3SignatureGenerator pSignatureGenerator) throws IOException,
+ SignatureException {
+
+ if (pLine == null) {
+ return;
+ }
+
+ final char[] chars = pLine.toCharArray();
+ int len = chars.length;
+
+ while (len > 0) {
+ if (!Character.isWhitespace(chars[len - 1])) {
+ break;
+ }
+ len--;
+ }
+
+ final byte[] data = pLine.substring(0, len).getBytes("UTF-8");
+
+ if (pArmoredOutput != null) {
+ pArmoredOutput.write(data);
+ }
+ pSignatureGenerator.update(data);
+ }
+
+ // taken from ClearSignedFileProcessor in BC
+ private static void processLine(PGPSignature sig, byte[] line) throws SignatureException,
+ IOException {
+ int length = getLengthWithoutWhiteSpace(line);
+ if (length > 0) {
+ sig.update(line, 0, length);
+ }
+ }
+
+ private static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn)
+ throws IOException {
+ bOut.reset();
+
+ int lookAhead = -1;
+ int ch;
+
+ while ((ch = fIn.read()) >= 0) {
+ bOut.write(ch);
+ if (ch == '\r' || ch == '\n') {
+ lookAhead = readPassedEOL(bOut, ch, fIn);
+ break;
+ }
+ }
+
+ return lookAhead;
+ }
+
+ private static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn)
+ throws IOException {
+ bOut.reset();
+
+ int ch = lookAhead;
+
+ do {
+ bOut.write(ch);
+ if (ch == '\r' || ch == '\n') {
+ lookAhead = readPassedEOL(bOut, ch, fIn);
+ break;
+ }
+ } while ((ch = fIn.read()) >= 0);
+
+ if (ch < 0) {
+ lookAhead = -1;
+ }
+
+ return lookAhead;
+ }
+
+ private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn)
+ throws IOException {
+ int lookAhead = fIn.read();
+
+ if (lastCh == '\r' && lookAhead == '\n') {
+ bOut.write(lookAhead);
+ lookAhead = fIn.read();
+ }
+
+ return lookAhead;
+ }
+
+ private static int getLengthWithoutSeparator(byte[] line) {
+ int end = line.length - 1;
+
+ while (end >= 0 && isLineEnding(line[end])) {
+ end--;
+ }
+
+ return end + 1;
+ }
+
+ private static boolean isLineEnding(byte b) {
+ return b == '\r' || b == '\n';
+ }
+
+ private static int getLengthWithoutWhiteSpace(byte[] line) {
+ int end = line.length - 1;
+
+ while (end >= 0 && isWhiteSpace(line[end])) {
+ end--;
+ }
+
+ return end + 1;
+ }
+
+ private static boolean isWhiteSpace(byte b) {
+ return b == '\r' || b == '\n' || b == '\t' || b == ' ';
+ }
+
+ private static byte[] getLineSeparator() {
+ String nl = System.getProperty("line.separator");
+ byte[] nlBytes = new byte[nl.length()];
+
+ for (int i = 0; i != nlBytes.length; i++) {
+ nlBytes[i] = (byte) nl.charAt(i);
+ }
+
+ return nlBytes;
+ }
+
+ public static boolean isReleaseVersion(Context context) {
+ try {
+ PackageInfo pi = context.getPackageManager().getPackageInfo(Constants.PACKAGE_NAME, 0);
+ if (pi.versionCode % 100 == 99) {
+ return true;
+ } else {
+ return false;
+ }
+ } catch (NameNotFoundException e) {
+ // impossible!
+ return false;
+ }
+ }
+
+ public static String getVersion(Context context) {
+ String version = null;
+ try {
+ PackageInfo pi = context.getPackageManager().getPackageInfo(Constants.PACKAGE_NAME, 0);
+ version = pi.versionName;
+ return version;
+ } catch (NameNotFoundException e) {
+ Log.e(Constants.TAG, "Version could not be retrieved!", e);
+ return "0.0.0";
+ }
+ }
+
+ public static String getFullVersion(Context context) {
+ return "APG v" + getVersion(context);
+ }
+
+ /**
+ * Generate a random filename
+ *
+ * @param length
+ * @return
+ */
+ public static String generateRandomFilename(int length) {
+ SecureRandom random = new SecureRandom();
+
+ byte bytes[] = new byte[length];
+ random.nextBytes(bytes);
+ String result = "";
+ for (int i = 0; i < length; ++i) {
+ int v = (bytes[i] + 256) % 64;
+ if (v < 10) {
+ result += (char) ('0' + v);
+ } else if (v < 36) {
+ result += (char) ('A' + v - 10);
+ } else if (v < 62) {
+ result += (char) ('a' + v - 36);
+ } else if (v == 62) {
+ result += '_';
+ } else if (v == 63) {
+ result += '.';
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Go once through stream to get length of stream. The length is later used to display progress
+ * when encrypting/decrypting
+ *
+ * @param in
+ * @return
+ * @throws IOException
+ */
+ public static long getLengthOfStream(InputStream in) throws IOException {
+ long size = 0;
+ long n = 0;
+ byte dummy[] = new byte[0x10000];
+ while ((n = in.read(dummy)) > 0) {
+ size += n;
+ }
+ return size;
+ }
+
+ /**
+ * Deletes file securely by overwriting it with random data before deleting it.
+ *
+ * TODO: Does this really help on flash storage?
+ *
+ * @param context
+ * @param progress
+ * @param file
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public static void deleteFileSecurely(Context context, ProgressDialogUpdater progress, File file)
+ throws FileNotFoundException, IOException {
+ long length = file.length();
+ SecureRandom random = new SecureRandom();
+ RandomAccessFile raf = new RandomAccessFile(file, "rws");
+ raf.seek(0);
+ raf.getFilePointer();
+ byte[] data = new byte[1 << 16];
+ int pos = 0;
+ String msg = context.getString(R.string.progress_deletingSecurely, file.getName());
+ while (pos < length) {
+ if (progress != null)
+ progress.setProgress(msg, (int) (100 * pos / length), 100);
+ random.nextBytes(data);
+ raf.write(data);
+ pos += data.length;
+ }
+ raf.close();
+ file.delete();
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/Preferences.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/Preferences.java
new file mode 100644
index 000000000..121bae6ef
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/Preferences.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.helper;
+
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.util.Vector;
+
+/**
+ * Singleton Implementation of a Preference Helper
+ */
+public class Preferences {
+ private static Preferences mPreferences;
+ private SharedPreferences mSharedPreferences;
+
+ public static synchronized Preferences getPreferences(Context context) {
+ return getPreferences(context, false);
+ }
+
+ public static synchronized Preferences getPreferences(Context context, boolean force_new) {
+ if (mPreferences == null || force_new) {
+ mPreferences = new Preferences(context);
+ }
+ return mPreferences;
+ }
+
+ private Preferences(Context context) {
+ mSharedPreferences = context.getSharedPreferences("APG.main", Context.MODE_PRIVATE);
+ }
+
+ public String getLanguage() {
+ return mSharedPreferences.getString(Constants.pref.LANGUAGE, "");
+ }
+
+ public void setLanguage(String value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putString(Constants.pref.LANGUAGE, value);
+ editor.commit();
+ }
+
+ public long getPassPhraseCacheTtl() {
+ int ttl = mSharedPreferences.getInt(Constants.pref.PASS_PHRASE_CACHE_TTL, 180);
+ // fix the value if it was set to "never" in previous versions, which currently is not
+ // supported
+ if (ttl == 0) {
+ ttl = 180;
+ }
+ return (long) ttl;
+ }
+
+ public void setPassPhraseCacheTtl(int value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putInt(Constants.pref.PASS_PHRASE_CACHE_TTL, value);
+ editor.commit();
+ }
+
+ public int getDefaultEncryptionAlgorithm() {
+ return mSharedPreferences.getInt(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM,
+ PGPEncryptedData.AES_256);
+ }
+
+ public void setDefaultEncryptionAlgorithm(int value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putInt(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM, value);
+ editor.commit();
+ }
+
+ public int getDefaultHashAlgorithm() {
+ return mSharedPreferences.getInt(Constants.pref.DEFAULT_HASH_ALGORITHM,
+ HashAlgorithmTags.SHA256);
+ }
+
+ public void setDefaultHashAlgorithm(int value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putInt(Constants.pref.DEFAULT_HASH_ALGORITHM, value);
+ editor.commit();
+ }
+
+ public int getDefaultMessageCompression() {
+ return mSharedPreferences.getInt(Constants.pref.DEFAULT_MESSAGE_COMPRESSION,
+ Id.choice.compression.zlib);
+ }
+
+ public void setDefaultMessageCompression(int value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putInt(Constants.pref.DEFAULT_MESSAGE_COMPRESSION, value);
+ editor.commit();
+ }
+
+ public int getDefaultFileCompression() {
+ return mSharedPreferences.getInt(Constants.pref.DEFAULT_FILE_COMPRESSION,
+ Id.choice.compression.none);
+ }
+
+ public void setDefaultFileCompression(int value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putInt(Constants.pref.DEFAULT_FILE_COMPRESSION, value);
+ editor.commit();
+ }
+
+ public boolean getDefaultAsciiArmour() {
+ return mSharedPreferences.getBoolean(Constants.pref.DEFAULT_ASCII_ARMOUR, false);
+ }
+
+ public void setDefaultAsciiArmour(boolean value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Constants.pref.DEFAULT_ASCII_ARMOUR, value);
+ editor.commit();
+ }
+
+ public boolean getForceV3Signatures() {
+ return mSharedPreferences.getBoolean(Constants.pref.FORCE_V3_SIGNATURES, false);
+ }
+
+ public void setForceV3Signatures(boolean value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putBoolean(Constants.pref.FORCE_V3_SIGNATURES, value);
+ editor.commit();
+ }
+
+ public String[] getKeyServers() {
+ String rawData = mSharedPreferences.getString(Constants.pref.KEY_SERVERS,
+ Constants.defaults.KEY_SERVERS);
+ Vector<String> servers = new Vector<String>();
+ String chunks[] = rawData.split(",");
+ for (int i = 0; i < chunks.length; ++i) {
+ String tmp = chunks[i].trim();
+ if (tmp.length() > 0) {
+ servers.add(tmp);
+ }
+ }
+ return servers.toArray(chunks);
+ }
+
+ public void setKeyServers(String[] value) {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ String rawData = "";
+ for (int i = 0; i < value.length; ++i) {
+ String tmp = value[i].trim();
+ if (tmp.length() == 0) {
+ continue;
+ }
+ if (!"".equals(rawData)) {
+ rawData += ",";
+ }
+ rawData += tmp;
+ }
+ editor.putString(Constants.pref.KEY_SERVERS, rawData);
+ editor.commit();
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainContract.java
new file mode 100644
index 000000000..3d4dddea5
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainContract.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+import org.sufficientlysecure.keychain.Constants;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+public class KeychainContract {
+
+ interface KeyRingsColumns {
+ String MASTER_KEY_ID = "master_key_id"; // not a database id
+ String TYPE = "type"; // see KeyTypes
+ String KEY_RING_DATA = "key_ring_data"; // PGPPublicKeyRing / PGPSecretKeyRing blob
+ }
+
+ interface KeysColumns {
+ String KEY_ID = "key_id"; // not a database id
+ String TYPE = "type"; // see KeyTypes
+ String IS_MASTER_KEY = "is_master_key";
+ String ALGORITHM = "algorithm";
+ String KEY_SIZE = "key_size";
+ String CAN_SIGN = "can_sign";
+ String CAN_ENCRYPT = "can_encrypt";
+ String IS_REVOKED = "is_revoked";
+ String CREATION = "creation";
+ String EXPIRY = "expiry";
+ String KEY_RING_ROW_ID = "key_ring_row_id"; // foreign key to key_rings._ID
+ String KEY_DATA = "key_data"; // PGPPublicKey / PGPSecretKey blob
+ String RANK = "rank";
+ }
+
+ interface UserIdsColumns {
+ String KEY_RING_ROW_ID = "key_ring_row_id"; // foreign key to key_rings._ID
+ String USER_ID = "user_id"; // not a database id
+ String RANK = "rank";
+ }
+
+ public static final class KeyTypes {
+ public static final int PUBLIC = 0;
+ public static final int SECRET = 1;
+ }
+
+ public static final String CONTENT_AUTHORITY_EXTERNAL = Constants.PACKAGE_NAME;
+ public static final String CONTENT_AUTHORITY_INTERNAL = Constants.PACKAGE_NAME + ".internal";
+
+ private static final Uri BASE_CONTENT_URI_INTERNAL = Uri.parse("content://"
+ + CONTENT_AUTHORITY_INTERNAL);
+
+ public static final String BASE_KEY_RINGS = "key_rings";
+ public static final String BASE_DATA = "data";
+
+ public static final String PATH_PUBLIC = "public";
+ public static final String PATH_SECRET = "secret";
+
+ public static final String PATH_BY_MASTER_KEY_ID = "master_key_id";
+ public static final String PATH_BY_KEY_ID = "key_id";
+ public static final String PATH_BY_EMAILS = "emails";
+ public static final String PATH_BY_LIKE_EMAIL = "like_email";
+
+ public static final String PATH_USER_IDS = "user_ids";
+ public static final String PATH_KEYS = "keys";
+
+ public static class KeyRings implements KeyRingsColumns, BaseColumns {
+ public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
+ .appendPath(BASE_KEY_RINGS).build();
+
+ /** Use if multiple items get returned */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.key_ring";
+
+ /** 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 buildPublicKeyRingsUri() {
+ return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).build();
+ }
+
+ public static Uri buildPublicKeyRingsUri(String keyRingRowId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId).build();
+ }
+
+ public static Uri buildPublicKeyRingsByMasterKeyIdUri(String masterKeyId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC)
+ .appendPath(PATH_BY_MASTER_KEY_ID).appendPath(masterKeyId).build();
+ }
+
+ public static Uri buildPublicKeyRingsByKeyIdUri(String keyId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_KEY_ID)
+ .appendPath(keyId).build();
+ }
+
+ public static Uri buildPublicKeyRingsByEmailsUri(String emails) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_EMAILS)
+ .appendPath(emails).build();
+ }
+
+ public static Uri buildPublicKeyRingsByLikeEmailUri(String emails) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(PATH_BY_LIKE_EMAIL)
+ .appendPath(emails).build();
+ }
+
+ public static Uri buildSecretKeyRingsUri() {
+ return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).build();
+ }
+
+ public static Uri buildSecretKeyRingsUri(String keyRingRowId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId).build();
+ }
+
+ public static Uri buildSecretKeyRingsByMasterKeyIdUri(String masterKeyId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_SECRET)
+ .appendPath(PATH_BY_MASTER_KEY_ID).appendPath(masterKeyId).build();
+ }
+
+ public static Uri buildSecretKeyRingsByKeyIdUri(String keyId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_KEY_ID)
+ .appendPath(keyId).build();
+ }
+
+ public static Uri buildSecretKeyRingsByEmailsUri(String emails) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_EMAILS)
+ .appendPath(emails).build();
+ }
+
+ public static Uri buildSecretKeyRingsByLikeEmails(String emails) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(PATH_BY_LIKE_EMAIL)
+ .appendPath(emails).build();
+ }
+ }
+
+ public static class Keys implements KeysColumns, BaseColumns {
+ public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
+ .appendPath(BASE_KEY_RINGS).build();
+
+ /** Use if multiple items get returned */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.key";
+
+ /** Use if a single item is returned */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.key";
+
+ public static Uri buildPublicKeysUri(String keyRingRowId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
+ .appendPath(PATH_KEYS).build();
+ }
+
+ public static Uri buildPublicKeysUri(String keyRingRowId, String keyRowId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
+ .appendPath(PATH_KEYS).appendPath(keyRowId).build();
+ }
+
+ public static Uri buildSecretKeysUri(String keyRingRowId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
+ .appendPath(PATH_KEYS).build();
+ }
+
+ public static Uri buildSecretKeysUri(String keyRingRowId, String keyRowId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
+ .appendPath(PATH_KEYS).appendPath(keyRowId).build();
+ }
+ }
+
+ public static class UserIds implements UserIdsColumns, BaseColumns {
+ public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
+ .appendPath(BASE_KEY_RINGS).build();
+
+ /** Use if multiple items get returned */
+ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.thialfihar.apg.user_id";
+
+ /** Use if a single item is returned */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.thialfihar.apg.user_id";
+
+ public static Uri buildPublicUserIdsUri(String keyRingRowId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
+ .appendPath(PATH_USER_IDS).build();
+ }
+
+ public static Uri buildPublicUserIdsUri(String keyRingRowId, String userIdRowId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_PUBLIC).appendPath(keyRingRowId)
+ .appendPath(PATH_USER_IDS).appendPath(userIdRowId).build();
+ }
+
+ public static Uri buildSecretUserIdsUri(String keyRingRowId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
+ .appendPath(PATH_USER_IDS).build();
+ }
+
+ public static Uri buildSecretUserIdsUri(String keyRingRowId, String userIdRowId) {
+ return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId)
+ .appendPath(PATH_USER_IDS).appendPath(userIdRowId).build();
+ }
+ }
+
+ public static class DataStream {
+ public static final Uri CONTENT_URI = BASE_CONTENT_URI_INTERNAL.buildUpon()
+ .appendPath(BASE_DATA).build();
+
+ public static Uri buildDataStreamUri(String streamFilename) {
+ return CONTENT_URI.buildUpon().appendPath(streamFilename).build();
+ }
+ }
+
+ private KeychainContract() {
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainDatabase.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
new file mode 100644
index 000000000..4a2aeeb80
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainDatabase.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIdsColumns;
+import org.sufficientlysecure.keychain.util.Log;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.provider.BaseColumns;
+
+
+public class KeychainDatabase extends SQLiteOpenHelper {
+ private static final String DATABASE_NAME = "apg.db";
+ private static final int DATABASE_VERSION = 3;
+
+ public interface Tables {
+ String KEY_RINGS = "key_rings";
+ String KEYS = "keys";
+ String USER_IDS = "user_ids";
+ }
+
+ private static final String CREATE_KEY_RINGS = "CREATE TABLE IF NOT EXISTS " + Tables.KEY_RINGS
+ + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ + KeyRingsColumns.MASTER_KEY_ID + " INT64, " + KeyRingsColumns.TYPE + " INTEGER, "
+ + KeyRingsColumns.KEY_RING_DATA + " BLOB)";
+
+ private static final String CREATE_KEYS = "CREATE TABLE IF NOT EXISTS " + Tables.KEYS + " ("
+ + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + KeysColumns.KEY_ID
+ + " INT64, " + KeysColumns.TYPE + " INTEGER, " + KeysColumns.IS_MASTER_KEY
+ + " INTEGER, " + KeysColumns.ALGORITHM + " INTEGER, " + KeysColumns.KEY_SIZE
+ + " INTEGER, " + KeysColumns.CAN_SIGN + " INTEGER, " + KeysColumns.CAN_ENCRYPT
+ + " INTEGER, " + KeysColumns.IS_REVOKED + " INTEGER, " + KeysColumns.CREATION
+ + " INTEGER, " + KeysColumns.EXPIRY + " INTEGER, " + KeysColumns.KEY_DATA + " BLOB,"
+ + KeysColumns.RANK + " INTEGER, " + KeysColumns.KEY_RING_ROW_ID
+ + " INTEGER NOT NULL, FOREIGN KEY(" + KeysColumns.KEY_RING_ROW_ID + ") REFERENCES "
+ + Tables.KEY_RINGS + "(" + BaseColumns._ID + ") ON DELETE CASCADE)";
+
+ private static final String CREATE_USER_IDS = "CREATE TABLE IF NOT EXISTS " + Tables.USER_IDS
+ + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ + UserIdsColumns.USER_ID + " TEXT, " + UserIdsColumns.RANK + " INTEGER, "
+ + UserIdsColumns.KEY_RING_ROW_ID + " INTEGER NOT NULL, FOREIGN KEY("
+ + UserIdsColumns.KEY_RING_ROW_ID + ") REFERENCES " + Tables.KEY_RINGS + "("
+ + BaseColumns._ID + ") ON DELETE CASCADE)";
+
+ KeychainDatabase(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ Log.w(Constants.TAG, "Creating database...");
+
+ db.execSQL(CREATE_KEY_RINGS);
+ db.execSQL(CREATE_KEYS);
+ db.execSQL(CREATE_USER_IDS);
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ super.onOpen(db);
+ if (!db.isReadOnly()) {
+ // Enable foreign key constraints
+ db.execSQL("PRAGMA foreign_keys=ON;");
+ }
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.w(Constants.TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
+
+ // Upgrade from oldVersion through all methods to newest one
+ for (int version = oldVersion; version < newVersion; ++version) {
+ Log.w(Constants.TAG, "Upgrading database to version " + version);
+
+ switch (version) {
+
+ default:
+ break;
+
+ }
+ }
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProvider.java
new file mode 100644
index 000000000..9d531dceb
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProvider.java
@@ -0,0 +1,852 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingsColumns;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyTypes;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeysColumns;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIdsColumns;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
+import org.sufficientlysecure.keychain.util.Log;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+import android.text.TextUtils;
+
+public class KeychainProvider extends ContentProvider {
+ private static final int PUBLIC_KEY_RING = 101;
+ private static final int PUBLIC_KEY_RING_BY_ROW_ID = 102;
+ private static final int PUBLIC_KEY_RING_BY_MASTER_KEY_ID = 103;
+ private static final int PUBLIC_KEY_RING_BY_KEY_ID = 104;
+ private static final int PUBLIC_KEY_RING_BY_EMAILS = 105;
+ private static final int PUBLIC_KEY_RING_BY_LIKE_EMAIL = 106;
+
+ private static final int PUBLIC_KEY_RING_KEY = 111;
+ private static final int PUBLIC_KEY_RING_KEY_BY_ROW_ID = 112;
+
+ private static final int PUBLIC_KEY_RING_USER_ID = 121;
+ private static final int PUBLIC_KEY_RING_USER_ID_BY_ROW_ID = 122;
+
+ private static final int SECRET_KEY_RING = 201;
+ private static final int SECRET_KEY_RING_BY_ROW_ID = 202;
+ private static final int SECRET_KEY_RING_BY_MASTER_KEY_ID = 203;
+ private static final int SECRET_KEY_RING_BY_KEY_ID = 204;
+ private static final int SECRET_KEY_RING_BY_EMAILS = 205;
+ private static final int SECRET_KEY_RING_BY_LIKE_EMAIL = 206;
+
+ private static final int SECRET_KEY_RING_KEY = 211;
+ private static final int SECRET_KEY_RING_KEY_BY_ROW_ID = 212;
+
+ private static final int SECRET_KEY_RING_USER_ID = 221;
+ private static final int SECRET_KEY_RING_USER_ID_BY_ROW_ID = 222;
+
+ private static final int DATA_STREAM = 301;
+
+ protected boolean mInternalProvider;
+ protected UriMatcher mUriMatcher;
+
+ /**
+ * Build and return a {@link UriMatcher} that catches all {@link Uri} variations supported by
+ * this {@link ContentProvider}.
+ */
+ protected UriMatcher buildUriMatcher(boolean internalProvider) {
+ final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ String authority;
+ if (internalProvider) {
+ authority = KeychainContract.CONTENT_AUTHORITY_INTERNAL;
+ } else {
+ authority = KeychainContract.CONTENT_AUTHORITY_EXTERNAL;
+ }
+
+ /**
+ * public key rings
+ *
+ * <pre>
+ * key_rings/public
+ * key_rings/public/#
+ * key_rings/public/master_key_id/_
+ * key_rings/public/key_id/_
+ * key_rings/public/emails/_
+ * key_rings/public/like_email/_
+ * </pre>
+ */
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC,
+ PUBLIC_KEY_RING);
+ matcher.addURI(authority,
+ KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/#",
+ PUBLIC_KEY_RING_BY_ROW_ID);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/"
+ + KeychainContract.PATH_BY_MASTER_KEY_ID + "/*", PUBLIC_KEY_RING_BY_MASTER_KEY_ID);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/"
+ + KeychainContract.PATH_BY_KEY_ID + "/*", PUBLIC_KEY_RING_BY_KEY_ID);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/"
+ + KeychainContract.PATH_BY_EMAILS + "/*", PUBLIC_KEY_RING_BY_EMAILS);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/"
+ + KeychainContract.PATH_BY_EMAILS, PUBLIC_KEY_RING_BY_EMAILS); // without emails
+ // specified
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC + "/"
+ + KeychainContract.PATH_BY_LIKE_EMAIL + "/*", PUBLIC_KEY_RING_BY_LIKE_EMAIL);
+
+ /**
+ * public keys
+ *
+ * <pre>
+ * key_rings/public/#/keys
+ * key_rings/public/#/keys/#
+ * </pre>
+ */
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC
+ + "/#/" + KeychainContract.PATH_KEYS, PUBLIC_KEY_RING_KEY);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC
+ + "/#/" + KeychainContract.PATH_KEYS + "/#", PUBLIC_KEY_RING_KEY_BY_ROW_ID);
+
+ /**
+ * public user ids
+ *
+ * <pre>
+ * key_rings/public/#/user_ids
+ * key_rings/public/#/user_ids/#
+ * </pre>
+ */
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC
+ + "/#/" + KeychainContract.PATH_USER_IDS, PUBLIC_KEY_RING_USER_ID);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_PUBLIC
+ + "/#/" + KeychainContract.PATH_USER_IDS + "/#", PUBLIC_KEY_RING_USER_ID_BY_ROW_ID);
+
+ /**
+ * secret key rings
+ *
+ * <pre>
+ * key_rings/secret
+ * key_rings/secret/#
+ * key_rings/secret/master_key_id/_
+ * key_rings/secret/key_id/_
+ * key_rings/secret/emails/_
+ * key_rings/secret/like_email/_
+ * </pre>
+ */
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET,
+ SECRET_KEY_RING);
+ matcher.addURI(authority,
+ KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/#",
+ SECRET_KEY_RING_BY_ROW_ID);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/"
+ + KeychainContract.PATH_BY_MASTER_KEY_ID + "/*", SECRET_KEY_RING_BY_MASTER_KEY_ID);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/"
+ + KeychainContract.PATH_BY_KEY_ID + "/*", SECRET_KEY_RING_BY_KEY_ID);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/"
+ + KeychainContract.PATH_BY_EMAILS + "/*", SECRET_KEY_RING_BY_EMAILS);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/"
+ + KeychainContract.PATH_BY_EMAILS, SECRET_KEY_RING_BY_EMAILS); // without emails
+ // specified
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET + "/"
+ + KeychainContract.PATH_BY_LIKE_EMAIL + "/*", SECRET_KEY_RING_BY_LIKE_EMAIL);
+
+ /**
+ * secret keys
+ *
+ * <pre>
+ * key_rings/secret/#/keys
+ * key_rings/secret/#/keys/#
+ * </pre>
+ */
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET
+ + "/#/" + KeychainContract.PATH_KEYS, SECRET_KEY_RING_KEY);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET
+ + "/#/" + KeychainContract.PATH_KEYS + "/#", SECRET_KEY_RING_KEY_BY_ROW_ID);
+
+ /**
+ * secret user ids
+ *
+ * <pre>
+ * key_rings/secret/#/user_ids
+ * key_rings/secret/#/user_ids/#
+ * </pre>
+ */
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET
+ + "/#/" + KeychainContract.PATH_USER_IDS, SECRET_KEY_RING_USER_ID);
+ matcher.addURI(authority, KeychainContract.BASE_KEY_RINGS + "/" + KeychainContract.PATH_SECRET
+ + "/#/" + KeychainContract.PATH_USER_IDS + "/#", SECRET_KEY_RING_USER_ID_BY_ROW_ID);
+
+ /**
+ * data stream
+ *
+ * <pre>
+ * data / _
+ * </pre>
+ */
+ matcher.addURI(authority, KeychainContract.BASE_DATA + "/*", DATA_STREAM);
+
+ return matcher;
+ }
+
+ private KeychainDatabase mApgDatabase;
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onCreate() {
+ mUriMatcher = buildUriMatcher(mInternalProvider);
+ mApgDatabase = new KeychainDatabase(getContext());
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getType(Uri uri) {
+ final int match = mUriMatcher.match(uri);
+ switch (match) {
+ case PUBLIC_KEY_RING:
+ case PUBLIC_KEY_RING_BY_EMAILS:
+ case PUBLIC_KEY_RING_BY_LIKE_EMAIL:
+ case SECRET_KEY_RING:
+ case SECRET_KEY_RING_BY_EMAILS:
+ case SECRET_KEY_RING_BY_LIKE_EMAIL:
+ return KeyRings.CONTENT_TYPE;
+
+ case PUBLIC_KEY_RING_BY_ROW_ID:
+ case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
+ case PUBLIC_KEY_RING_BY_KEY_ID:
+ case SECRET_KEY_RING_BY_ROW_ID:
+ case SECRET_KEY_RING_BY_MASTER_KEY_ID:
+ case SECRET_KEY_RING_BY_KEY_ID:
+ return KeyRings.CONTENT_ITEM_TYPE;
+
+ case PUBLIC_KEY_RING_KEY:
+ case SECRET_KEY_RING_KEY:
+ return Keys.CONTENT_TYPE;
+
+ case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
+ case SECRET_KEY_RING_KEY_BY_ROW_ID:
+ return Keys.CONTENT_ITEM_TYPE;
+
+ case PUBLIC_KEY_RING_USER_ID:
+ case SECRET_KEY_RING_USER_ID:
+ return UserIds.CONTENT_TYPE;
+
+ case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
+ case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
+ return UserIds.CONTENT_ITEM_TYPE;
+
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ }
+
+ /**
+ * Returns type of the query (secret/public)
+ *
+ * @param uri
+ * @return
+ */
+ private int getKeyType(int match) {
+ int type;
+ switch (match) {
+ case PUBLIC_KEY_RING:
+ case PUBLIC_KEY_RING_BY_ROW_ID:
+ case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
+ case PUBLIC_KEY_RING_BY_KEY_ID:
+ case PUBLIC_KEY_RING_BY_EMAILS:
+ case PUBLIC_KEY_RING_BY_LIKE_EMAIL:
+ case PUBLIC_KEY_RING_KEY:
+ case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
+ case PUBLIC_KEY_RING_USER_ID:
+ case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
+ type = KeyTypes.PUBLIC;
+ break;
+
+ case SECRET_KEY_RING:
+ case SECRET_KEY_RING_BY_ROW_ID:
+ case SECRET_KEY_RING_BY_MASTER_KEY_ID:
+ case SECRET_KEY_RING_BY_KEY_ID:
+ case SECRET_KEY_RING_BY_EMAILS:
+ case SECRET_KEY_RING_BY_LIKE_EMAIL:
+ case SECRET_KEY_RING_KEY:
+ case SECRET_KEY_RING_KEY_BY_ROW_ID:
+ case SECRET_KEY_RING_USER_ID:
+ case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
+ type = KeyTypes.SECRET;
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unknown match " + match);
+
+ }
+
+ return type;
+ }
+
+ /**
+ * Set result of query to specific columns, don't show blob column for external content provider
+ *
+ * @return
+ */
+ private HashMap<String, String> getProjectionMapForKeyRings() {
+ HashMap<String, String> projectionMap = new HashMap<String, String>();
+
+ projectionMap.put(BaseColumns._ID, Tables.KEY_RINGS + "." + BaseColumns._ID);
+ projectionMap.put(KeyRingsColumns.MASTER_KEY_ID, Tables.KEY_RINGS + "."
+ + KeyRingsColumns.MASTER_KEY_ID);
+ // only give out keyRing blob when we are using the internal content provider
+ if (mInternalProvider) {
+ projectionMap.put(KeyRingsColumns.KEY_RING_DATA, Tables.KEY_RINGS + "."
+ + KeyRingsColumns.KEY_RING_DATA);
+ }
+ projectionMap.put(UserIdsColumns.USER_ID, Tables.USER_IDS + "." + UserIdsColumns.USER_ID);
+
+ return projectionMap;
+ }
+
+ /**
+ * Set result of query to specific columns, don't show blob column for external content provider
+ *
+ * @return
+ */
+ private HashMap<String, String> getProjectionMapForKeys() {
+ HashMap<String, String> projectionMap = new HashMap<String, String>();
+
+ projectionMap.put(BaseColumns._ID, BaseColumns._ID);
+ projectionMap.put(KeysColumns.KEY_ID, KeysColumns.KEY_ID);
+ projectionMap.put(KeysColumns.IS_MASTER_KEY, KeysColumns.IS_MASTER_KEY);
+ projectionMap.put(KeysColumns.ALGORITHM, KeysColumns.ALGORITHM);
+ projectionMap.put(KeysColumns.KEY_SIZE, KeysColumns.KEY_SIZE);
+ projectionMap.put(KeysColumns.CAN_SIGN, KeysColumns.CAN_SIGN);
+ projectionMap.put(KeysColumns.CAN_ENCRYPT, KeysColumns.CAN_ENCRYPT);
+ projectionMap.put(KeysColumns.IS_REVOKED, KeysColumns.IS_REVOKED);
+ projectionMap.put(KeysColumns.CREATION, KeysColumns.CREATION);
+ projectionMap.put(KeysColumns.EXPIRY, KeysColumns.EXPIRY);
+ projectionMap.put(KeysColumns.KEY_RING_ROW_ID, KeysColumns.KEY_RING_ROW_ID);
+ // only give out keyRing blob when we are using the internal content provider
+ if (mInternalProvider) {
+ projectionMap.put(KeysColumns.KEY_DATA, KeysColumns.KEY_DATA);
+ }
+ projectionMap.put(KeysColumns.RANK, KeysColumns.RANK);
+
+ return projectionMap;
+ }
+
+ /**
+ * Builds default query for keyRings: KeyRings table is joined with UserIds
+ *
+ * @param qb
+ * @param match
+ * @param isMasterKey
+ * @param sortOrder
+ * @return
+ */
+ private SQLiteQueryBuilder buildKeyRingQuery(SQLiteQueryBuilder qb, int match, String sortOrder) {
+ // public or secret keyring
+ qb.appendWhere(Tables.KEY_RINGS + "." + KeyRingsColumns.TYPE + " = ");
+ qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
+
+ // join keyrings with userIds to every keyring
+ qb.setTables(Tables.KEY_RINGS + " 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')");
+
+ qb.setProjectionMap(getProjectionMapForKeyRings());
+
+ return qb;
+ }
+
+ /**
+ * Builds default query for keyRings: KeyRings table is joined with Keys and UserIds
+ *
+ * @param qb
+ * @param match
+ * @param isMasterKey
+ * @param sortOrder
+ * @return
+ */
+ private SQLiteQueryBuilder buildKeyRingQueryWithKeys(SQLiteQueryBuilder qb, int match,
+ String sortOrder) {
+ // public or secret keyring
+ qb.appendWhere(Tables.KEY_RINGS + "." + KeyRingsColumns.TYPE + " = ");
+ qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
+
+ // join keyrings with keys and userIds to every keyring
+ qb.setTables(Tables.KEY_RINGS + " INNER JOIN " + Tables.KEYS + " ON " + "("
+ + Tables.KEY_RINGS + "." + BaseColumns._ID + " = " + Tables.KEYS + "."
+ + KeysColumns.KEY_RING_ROW_ID + ") " + " 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')");
+
+ qb.setProjectionMap(getProjectionMapForKeyRings());
+
+ return qb;
+ }
+
+ /** {@inheritDoc} */
+ @SuppressWarnings("deprecation")
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ Log.v(Constants.TAG, "query(uri=" + uri + ", proj=" + Arrays.toString(projection) + ")");
+
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ SQLiteDatabase db = mApgDatabase.getReadableDatabase();
+
+ int match = mUriMatcher.match(uri);
+
+ switch (match) {
+ case PUBLIC_KEY_RING:
+ case SECRET_KEY_RING:
+ qb = buildKeyRingQuery(qb, match, sortOrder);
+
+ if (TextUtils.isEmpty(sortOrder)) {
+ sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
+ }
+
+ break;
+
+ case PUBLIC_KEY_RING_BY_ROW_ID:
+ case SECRET_KEY_RING_BY_ROW_ID:
+ qb = buildKeyRingQuery(qb, match, sortOrder);
+
+ qb.appendWhere(" AND " + Tables.KEY_RINGS + "." + BaseColumns._ID + " = ");
+ qb.appendWhereEscapeString(uri.getLastPathSegment());
+
+ if (TextUtils.isEmpty(sortOrder)) {
+ sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
+ }
+
+ break;
+
+ case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
+ case SECRET_KEY_RING_BY_MASTER_KEY_ID:
+ qb = buildKeyRingQuery(qb, match, sortOrder);
+
+ qb.appendWhere(" AND " + Tables.KEY_RINGS + "." + KeyRingsColumns.MASTER_KEY_ID + " = ");
+ qb.appendWhereEscapeString(uri.getLastPathSegment());
+
+ if (TextUtils.isEmpty(sortOrder)) {
+ sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
+ }
+
+ break;
+
+ case SECRET_KEY_RING_BY_KEY_ID:
+ case PUBLIC_KEY_RING_BY_KEY_ID:
+ qb = buildKeyRingQueryWithKeys(qb, match, sortOrder);
+
+ qb.appendWhere(" AND " + Tables.KEYS + "." + KeysColumns.KEY_ID + " = ");
+ qb.appendWhereEscapeString(uri.getLastPathSegment());
+
+ if (TextUtils.isEmpty(sortOrder)) {
+ sortOrder = Tables.USER_IDS + "." + UserIdsColumns.USER_ID + " ASC";
+ }
+
+ break;
+
+ case SECRET_KEY_RING_BY_EMAILS:
+ case PUBLIC_KEY_RING_BY_EMAILS:
+ qb = buildKeyRingQuery(qb, match, sortOrder);
+
+ String emails = uri.getLastPathSegment();
+ String chunks[] = emails.split(" *, *");
+ boolean gotCondition = false;
+ String emailWhere = "";
+ for (int i = 0; i < chunks.length; ++i) {
+ if (chunks[i].length() == 0) {
+ continue;
+ }
+ if (i != 0) {
+ emailWhere += " OR ";
+ }
+ emailWhere += "tmp." + UserIdsColumns.USER_ID + " LIKE ";
+ // match '*<email>', so it has to be at the *end* of the user id
+ emailWhere += DatabaseUtils.sqlEscapeString("%<" + chunks[i] + ">");
+ gotCondition = true;
+ }
+
+ if (gotCondition) {
+ qb.appendWhere(" AND EXISTS (SELECT tmp." + BaseColumns._ID + " FROM "
+ + Tables.USER_IDS + " AS tmp WHERE tmp." + UserIdsColumns.KEY_RING_ROW_ID
+ + " = " + Tables.KEY_RINGS + "." + BaseColumns._ID + " AND (" + emailWhere
+ + "))");
+ }
+
+ break;
+
+ case SECRET_KEY_RING_BY_LIKE_EMAIL:
+ case PUBLIC_KEY_RING_BY_LIKE_EMAIL:
+ qb = buildKeyRingQuery(qb, match, sortOrder);
+
+ String likeEmail = uri.getLastPathSegment();
+
+ String likeEmailWhere = "tmp." + UserIdsColumns.USER_ID + " LIKE "
+ + DatabaseUtils.sqlEscapeString("%<%" + likeEmail + "%>");
+
+ qb.appendWhere(" AND EXISTS (SELECT tmp." + BaseColumns._ID + " FROM "
+ + Tables.USER_IDS + " AS tmp WHERE tmp." + UserIdsColumns.KEY_RING_ROW_ID
+ + " = " + Tables.KEY_RINGS + "." + BaseColumns._ID + " AND (" + likeEmailWhere
+ + "))");
+
+ break;
+
+ case PUBLIC_KEY_RING_KEY:
+ case SECRET_KEY_RING_KEY:
+ qb.setTables(Tables.KEYS);
+ qb.appendWhere(KeysColumns.TYPE + " = ");
+ qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
+
+ qb.appendWhere(" AND " + KeysColumns.KEY_RING_ROW_ID + " = ");
+ qb.appendWhereEscapeString(uri.getPathSegments().get(2));
+
+ qb.setProjectionMap(getProjectionMapForKeys());
+
+ break;
+
+ case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
+ case SECRET_KEY_RING_KEY_BY_ROW_ID:
+ qb.setTables(Tables.KEYS);
+ qb.appendWhere(KeysColumns.TYPE + " = ");
+ qb.appendWhereEscapeString(Integer.toString(getKeyType(match)));
+
+ qb.appendWhere(" AND " + KeysColumns.KEY_RING_ROW_ID + " = ");
+ qb.appendWhereEscapeString(uri.getPathSegments().get(2));
+
+ qb.appendWhere(" AND " + BaseColumns._ID + " = ");
+ qb.appendWhereEscapeString(uri.getLastPathSegment());
+
+ qb.setProjectionMap(getProjectionMapForKeys());
+
+ break;
+
+ case PUBLIC_KEY_RING_USER_ID:
+ case SECRET_KEY_RING_USER_ID:
+ qb.setTables(Tables.USER_IDS);
+ qb.appendWhere(UserIdsColumns.KEY_RING_ROW_ID + " = ");
+ qb.appendWhereEscapeString(uri.getPathSegments().get(2));
+
+ break;
+
+ case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
+ case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
+ qb.setTables(Tables.USER_IDS);
+ qb.appendWhere(UserIdsColumns.KEY_RING_ROW_ID + " = ");
+ qb.appendWhereEscapeString(uri.getPathSegments().get(2));
+
+ qb.appendWhere(" AND " + BaseColumns._ID + " = ");
+ qb.appendWhereEscapeString(uri.getLastPathSegment());
+
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unknown URI " + uri);
+
+ }
+
+ // If no sort order is specified use the default
+ String orderBy;
+ if (TextUtils.isEmpty(sortOrder)) {
+ orderBy = null;
+ } else {
+ orderBy = sortOrder;
+ }
+
+ Cursor c = qb.query(db, projection, selection, selectionArgs, null, 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, null, null,
+ orderBy, null));
+ Log.d(Constants.TAG, "Cursor: " + DatabaseUtils.dumpCursorToString(c));
+ }
+
+ return c;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ Log.d(Constants.TAG, "insert(uri=" + uri + ", values=" + values.toString() + ")");
+
+ final SQLiteDatabase db = mApgDatabase.getWritableDatabase();
+
+ Uri rowUri = null;
+ long rowId = -1;
+ try {
+ final int match = mUriMatcher.match(uri);
+
+ switch (match) {
+ case PUBLIC_KEY_RING:
+ values.put(KeyRings.TYPE, KeyTypes.PUBLIC);
+
+ rowId = db.insertOrThrow(Tables.KEY_RINGS, null, values);
+ rowUri = KeyRings.buildPublicKeyRingsUri(Long.toString(rowId));
+
+ break;
+ case PUBLIC_KEY_RING_KEY:
+ values.put(Keys.TYPE, KeyTypes.PUBLIC);
+
+ rowId = db.insertOrThrow(Tables.KEYS, null, values);
+ rowUri = Keys.buildPublicKeysUri(Long.toString(rowId));
+
+ break;
+ case PUBLIC_KEY_RING_USER_ID:
+ rowId = db.insertOrThrow(Tables.USER_IDS, null, values);
+ rowUri = UserIds.buildPublicUserIdsUri(Long.toString(rowId));
+
+ break;
+ case SECRET_KEY_RING:
+ values.put(KeyRings.TYPE, KeyTypes.SECRET);
+
+ rowId = db.insertOrThrow(Tables.KEY_RINGS, null, values);
+ rowUri = KeyRings.buildSecretKeyRingsUri(Long.toString(rowId));
+
+ break;
+ case SECRET_KEY_RING_KEY:
+ values.put(Keys.TYPE, KeyTypes.SECRET);
+
+ rowId = db.insertOrThrow(Tables.KEYS, null, values);
+ rowUri = Keys.buildSecretKeysUri(Long.toString(rowId));
+
+ break;
+ case SECRET_KEY_RING_USER_ID:
+ rowId = db.insertOrThrow(Tables.USER_IDS, null, values);
+ rowUri = UserIds.buildSecretUserIdsUri(Long.toString(rowId));
+
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ } catch (SQLiteConstraintException e) {
+ Log.e(Constants.TAG, "Constraint exception on insert! Entry already existing?");
+ }
+
+ // notify of changes in db
+ getContext().getContentResolver().notifyChange(uri, null);
+
+ return rowUri;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ Log.v(Constants.TAG, "delete(uri=" + uri + ")");
+
+ final SQLiteDatabase db = mApgDatabase.getWritableDatabase();
+
+ int count;
+ final int match = mUriMatcher.match(uri);
+
+ String defaultSelection = null;
+ switch (match) {
+ case PUBLIC_KEY_RING_BY_ROW_ID:
+ case SECRET_KEY_RING_BY_ROW_ID:
+ defaultSelection = BaseColumns._ID + "=" + uri.getLastPathSegment();
+ // corresponding keys and userIds are deleted by ON DELETE CASCADE
+ count = db.delete(Tables.KEY_RINGS,
+ buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match), selection),
+ selectionArgs);
+ break;
+ case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
+ case SECRET_KEY_RING_BY_MASTER_KEY_ID:
+ defaultSelection = KeyRings.MASTER_KEY_ID + "=" + uri.getLastPathSegment();
+ // corresponding keys and userIds are deleted by ON DELETE CASCADE
+ count = db.delete(Tables.KEY_RINGS,
+ buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match), selection),
+ selectionArgs);
+ break;
+ case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
+ case SECRET_KEY_RING_KEY_BY_ROW_ID:
+ count = db.delete(Tables.KEYS,
+ buildDefaultKeysSelection(uri, getKeyType(match), selection), selectionArgs);
+ break;
+ case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
+ case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
+ count = db.delete(Tables.KEYS, buildDefaultUserIdsSelection(uri, selection),
+ selectionArgs);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+
+ // notify of changes in db
+ getContext().getContentResolver().notifyChange(uri, null);
+
+ return count;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ Log.v(Constants.TAG, "update(uri=" + uri + ", values=" + values.toString() + ")");
+
+ final SQLiteDatabase db = mApgDatabase.getWritableDatabase();
+
+ String defaultSelection = null;
+ int count = 0;
+ try {
+ final int match = mUriMatcher.match(uri);
+ switch (match) {
+ case PUBLIC_KEY_RING_BY_ROW_ID:
+ case SECRET_KEY_RING_BY_ROW_ID:
+ defaultSelection = BaseColumns._ID + "=" + uri.getLastPathSegment();
+
+ count = db.update(
+ Tables.KEY_RINGS,
+ values,
+ buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match),
+ selection), selectionArgs);
+ break;
+ case PUBLIC_KEY_RING_BY_MASTER_KEY_ID:
+ case SECRET_KEY_RING_BY_MASTER_KEY_ID:
+ defaultSelection = KeyRings.MASTER_KEY_ID + "=" + uri.getLastPathSegment();
+
+ count = db.update(
+ Tables.KEY_RINGS,
+ values,
+ buildDefaultKeyRingsSelection(defaultSelection, getKeyType(match),
+ selection), selectionArgs);
+ break;
+ case PUBLIC_KEY_RING_KEY_BY_ROW_ID:
+ case SECRET_KEY_RING_KEY_BY_ROW_ID:
+ count = db
+ .update(Tables.KEYS, values,
+ buildDefaultKeysSelection(uri, getKeyType(match), selection),
+ selectionArgs);
+ break;
+ case PUBLIC_KEY_RING_USER_ID_BY_ROW_ID:
+ case SECRET_KEY_RING_USER_ID_BY_ROW_ID:
+ count = db.update(Tables.USER_IDS, values,
+ buildDefaultUserIdsSelection(uri, selection), selectionArgs);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown uri: " + uri);
+ }
+ } catch (SQLiteConstraintException e) {
+ Log.e(Constants.TAG, "Constraint exception on update! Entry already existing?");
+ }
+
+ // notify of changes in db
+ getContext().getContentResolver().notifyChange(uri, null);
+
+ return count;
+ }
+
+ /**
+ * Build default selection statement for KeyRings. If no extra selection is specified only build
+ * where clause with rowId
+ *
+ * @param uri
+ * @param selection
+ * @return
+ */
+ private String buildDefaultKeyRingsSelection(String defaultSelection, Integer keyType,
+ String selection) {
+ String andType = "";
+ if (keyType != null) {
+ andType = " AND " + KeyRingsColumns.TYPE + "=" + keyType;
+ }
+
+ String andSelection = "";
+ if (!TextUtils.isEmpty(selection)) {
+ andSelection = " AND (" + selection + ")";
+ }
+
+ return defaultSelection + andType + andSelection;
+ }
+
+ /**
+ * Build default selection statement for Keys. If no extra selection is specified only build
+ * where clause with rowId
+ *
+ * @param uri
+ * @param selection
+ * @return
+ */
+ private String buildDefaultKeysSelection(Uri uri, Integer keyType, String selection) {
+ String rowId = uri.getLastPathSegment();
+
+ String foreignKeyRingRowId = uri.getPathSegments().get(2);
+ String andForeignKeyRing = " AND " + KeysColumns.KEY_RING_ROW_ID + " = "
+ + foreignKeyRingRowId;
+
+ String andType = "";
+ if (keyType != null) {
+ andType = " AND " + KeysColumns.TYPE + "=" + keyType;
+ }
+
+ String andSelection = "";
+ if (!TextUtils.isEmpty(selection)) {
+ andSelection = " AND (" + selection + ")";
+ }
+
+ return BaseColumns._ID + "=" + rowId + andForeignKeyRing + andType + andSelection;
+ }
+
+ /**
+ * Build default selection statement for UserIds. If no extra selection is specified only build
+ * where clause with rowId
+ *
+ * @param uri
+ * @param selection
+ * @return
+ */
+ private String buildDefaultUserIdsSelection(Uri uri, String selection) {
+ String rowId = uri.getLastPathSegment();
+
+ String foreignKeyRingRowId = uri.getPathSegments().get(2);
+ String andForeignKeyRing = " AND " + KeysColumns.KEY_RING_ROW_ID + " = "
+ + foreignKeyRingRowId;
+
+ String andSelection = "";
+ if (!TextUtils.isEmpty(selection)) {
+ andSelection = " AND (" + selection + ")";
+ }
+
+ return BaseColumns._ID + "=" + rowId + andForeignKeyRing + andSelection;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ int match = mUriMatcher.match(uri);
+ if (match != DATA_STREAM) {
+ throw new FileNotFoundException();
+ }
+ String fileName = uri.getLastPathSegment();
+ File file = new File(getContext().getFilesDir().getAbsolutePath(), fileName);
+ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderExternal.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderExternal.java
new file mode 100644
index 000000000..a62b85621
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderExternal.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+/**
+ * The same content provider as ApgProviderInternal except that it does not give out keyRing and key
+ * blob data when querying.
+ *
+ * This provider is exported with a readPermission in AndroidManifest.xml
+ */
+public class KeychainProviderExternal extends KeychainProvider {
+
+ public KeychainProviderExternal() {
+ mInternalProvider = false;
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderInternal.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderInternal.java
new file mode 100644
index 000000000..ffac2f7cc
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainProviderInternal.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+/**
+ * This provider is NOT exported in AndroidManifest.xml as it also return the actual secret keys
+ * from the database
+ */
+public class KeychainProviderInternal extends KeychainProvider {
+
+ public KeychainProviderInternal() {
+ mInternalProvider = true;
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobContract.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobContract.java
new file mode 100644
index 000000000..340d3c788
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobContract.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+import org.sufficientlysecure.keychain.Constants;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+public class KeychainServiceBlobContract {
+
+ interface BlobsColumns {
+ String KEY = "key";
+ }
+
+ public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".blobs";
+
+ private static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
+
+ public static class Blobs implements BlobsColumns, BaseColumns {
+ public static final Uri CONTENT_URI = BASE_CONTENT_URI;
+ }
+
+ private KeychainServiceBlobContract() {
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobDatabase.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobDatabase.java
new file mode 100644
index 000000000..386349c0e
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobDatabase.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2011 Markus Doits <markus.doits@googlemail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.provider.BaseColumns;
+
+import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.BlobsColumns;
+
+public class KeychainServiceBlobDatabase extends SQLiteOpenHelper {
+ private static final String DATABASE_NAME = "apg_blob.db";
+ private static final int DATABASE_VERSION = 2;
+
+ public static final String TABLE = "data";
+
+ public KeychainServiceBlobDatabase(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + TABLE + " ( " + BaseColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT, " + BlobsColumns.KEY + " TEXT NOT NULL)");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // no upgrade necessary yet
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobProvider.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobProvider.java
new file mode 100644
index 000000000..761bfb2fe
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainServiceBlobProvider.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2011 Markus Doits <markus.doits@googlemail.com>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.Blobs;
+import org.sufficientlysecure.keychain.provider.KeychainServiceBlobContract.BlobsColumns;
+import org.sufficientlysecure.keychain.util.Log;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+public class KeychainServiceBlobProvider extends ContentProvider {
+ private static final String STORE_PATH = Constants.path.APP_DIR + "/ApgBlobs";
+
+ private KeychainServiceBlobDatabase mBlobDatabase = null;
+
+ public KeychainServiceBlobProvider() {
+ File dir = new File(STORE_PATH);
+ dir.mkdirs();
+ }
+
+ @Override
+ public boolean onCreate() {
+ mBlobDatabase = new KeychainServiceBlobDatabase(getContext());
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Uri insert(Uri uri, ContentValues ignored) {
+ // ContentValues are actually ignored, because we want to store a blob with no more
+ // information but have to create an record with the password generated here first
+ ContentValues vals = new ContentValues();
+
+ // Insert a random key in the database. This has to provided by the caller when updating or
+ // getting the blob
+ String password = UUID.randomUUID().toString();
+ vals.put(BlobsColumns.KEY, password);
+
+ SQLiteDatabase db = mBlobDatabase.getWritableDatabase();
+ long newRowId = db.insert(KeychainServiceBlobDatabase.TABLE, null, vals);
+ Uri insertedUri = ContentUris.withAppendedId(Blobs.CONTENT_URI, newRowId);
+
+ return Uri.withAppendedPath(insertedUri, password);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws SecurityException,
+ FileNotFoundException {
+ Log.d(Constants.TAG, "openFile() called with uri: " + uri.toString() + " and mode: " + mode);
+
+ List<String> segments = uri.getPathSegments();
+ if (segments.size() < 2) {
+ throw new SecurityException("Password not found in URI");
+ }
+ String id = segments.get(0);
+ String key = segments.get(1);
+
+ Log.d(Constants.TAG, "Got id: " + id + " and key: " + key);
+
+ // get the data
+ SQLiteDatabase db = mBlobDatabase.getReadableDatabase();
+ Cursor result = db.query(KeychainServiceBlobDatabase.TABLE, new String[] { BaseColumns._ID },
+ BaseColumns._ID + " = ? and " + BlobsColumns.KEY + " = ?",
+ new String[] { id, key }, null, null, null);
+
+ if (result.getCount() == 0) {
+ // either the key is wrong or no id exists
+ throw new FileNotFoundException("No file found with that ID and/or password");
+ }
+
+ File targetFile = new File(STORE_PATH, id);
+ if (mode.equals("w")) {
+ Log.d(Constants.TAG, "Try to open file w");
+ if (!targetFile.exists()) {
+ try {
+ targetFile.createNewFile();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Got IEOException on creating new file", e);
+ throw new FileNotFoundException("Could not create file to write to");
+ }
+ }
+ return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_WRITE_ONLY
+ | ParcelFileDescriptor.MODE_TRUNCATE);
+ } else if (mode.equals("r")) {
+ Log.d(Constants.TAG, "Try to open file r");
+ if (!targetFile.exists()) {
+ throw new FileNotFoundException("Error: Could not find the file requested");
+ }
+ return ParcelFileDescriptor.open(targetFile, ParcelFileDescriptor.MODE_READ_ONLY);
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/ProviderHelper.java
new file mode 100644
index 000000000..36049a64c
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/ProviderHelper.java
@@ -0,0 +1,663 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.provider;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+
+import org.spongycastle.bcpg.ArmoredOutputStream;
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.PgpConversionHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.util.IterableIterator;
+import org.sufficientlysecure.keychain.util.Log;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.os.RemoteException;
+
+public class ProviderHelper {
+
+ /**
+ * Private helper method to get PGPKeyRing from database
+ *
+ * @param context
+ * @param queryUri
+ * @return
+ */
+ private static PGPKeyRing getPGPKeyRing(Context context, Uri queryUri) {
+ Cursor cursor = context.getContentResolver().query(queryUri,
+ new String[] { KeyRings._ID, KeyRings.KEY_RING_DATA }, null, null, null);
+
+ PGPKeyRing keyRing = null;
+ if (cursor != null && cursor.moveToFirst()) {
+ int keyRingDataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA);
+
+ byte[] data = cursor.getBlob(keyRingDataCol);
+ if (data != null) {
+ keyRing = PgpConversionHelper.BytesToPGPKeyRing(data);
+ }
+ }
+
+ if (cursor != null) {
+ cursor.close();
+ }
+
+ return keyRing;
+ }
+
+ /**
+ * Retrieves the actual PGPPublicKeyRing object from the database blob based on the rowId
+ *
+ * @param context
+ * @param rowId
+ * @return
+ */
+ public static PGPPublicKeyRing getPGPPublicKeyRingByRowId(Context context, long rowId) {
+ Uri queryUri = KeyRings.buildPublicKeyRingsUri(Long.toString(rowId));
+ return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri);
+ }
+
+ /**
+ * Retrieves the actual PGPPublicKeyRing object from the database blob based on the maserKeyId
+ *
+ * @param context
+ * @param masterKeyId
+ * @return
+ */
+ public static PGPPublicKeyRing getPGPPublicKeyRingByMasterKeyId(Context context,
+ long masterKeyId) {
+ Uri queryUri = KeyRings.buildPublicKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
+ return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri);
+ }
+
+ /**
+ * Retrieves the actual PGPPublicKeyRing object from the database blob associated with a key
+ * with this keyId
+ *
+ * @param context
+ * @param keyId
+ * @return
+ */
+ public static PGPPublicKeyRing getPGPPublicKeyRingByKeyId(Context context, long keyId) {
+ Uri queryUri = KeyRings.buildPublicKeyRingsByKeyIdUri(Long.toString(keyId));
+ return (PGPPublicKeyRing) getPGPKeyRing(context, queryUri);
+ }
+
+ /**
+ * Retrieves the actual PGPPublicKey object from the database blob associated with a key with
+ * this keyId
+ *
+ * @param context
+ * @param keyId
+ * @return
+ */
+ public static PGPPublicKey getPGPPublicKeyByKeyId(Context context, long keyId) {
+ PGPPublicKeyRing keyRing = getPGPPublicKeyRingByKeyId(context, keyId);
+ if (keyRing == null) {
+ return null;
+ }
+
+ return keyRing.getPublicKey(keyId);
+ }
+
+ /**
+ * Retrieves the actual PGPSecretKeyRing object from the database blob based on the rowId
+ *
+ * @param context
+ * @param rowId
+ * @return
+ */
+ public static PGPSecretKeyRing getPGPSecretKeyRingByRowId(Context context, long rowId) {
+ Uri queryUri = KeyRings.buildSecretKeyRingsUri(Long.toString(rowId));
+ return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri);
+ }
+
+ /**
+ * Retrieves the actual PGPSecretKeyRing object from the database blob based on the maserKeyId
+ *
+ * @param context
+ * @param masterKeyId
+ * @return
+ */
+ public static PGPSecretKeyRing getPGPSecretKeyRingByMasterKeyId(Context context,
+ long masterKeyId) {
+ Uri queryUri = KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
+ return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri);
+ }
+
+ /**
+ * Retrieves the actual PGPSecretKeyRing object from the database blob associated with a key
+ * with this keyId
+ *
+ * @param context
+ * @param keyId
+ * @return
+ */
+ public static PGPSecretKeyRing getPGPSecretKeyRingByKeyId(Context context, long keyId) {
+ Uri queryUri = KeyRings.buildSecretKeyRingsByKeyIdUri(Long.toString(keyId));
+ return (PGPSecretKeyRing) getPGPKeyRing(context, queryUri);
+ }
+
+ /**
+ * Retrieves the actual PGPSecretKey object from the database blob associated with a key with
+ * this keyId
+ *
+ * @param context
+ * @param keyId
+ * @return
+ */
+ public static PGPSecretKey getPGPSecretKeyByKeyId(Context context, long keyId) {
+ PGPSecretKeyRing keyRing = getPGPSecretKeyRingByKeyId(context, keyId);
+ if (keyRing == null) {
+ return null;
+ }
+
+ return keyRing.getSecretKey(keyId);
+ }
+
+ /**
+ * Saves PGPPublicKeyRing with its keys and userIds in DB
+ *
+ * @param context
+ * @param keyRing
+ * @return
+ * @throws IOException
+ * @throws GeneralException
+ */
+ @SuppressWarnings("unchecked")
+ public static void saveKeyRing(Context context, PGPPublicKeyRing keyRing) throws IOException {
+ PGPPublicKey masterKey = keyRing.getPublicKey();
+ long masterKeyId = masterKey.getKeyID();
+
+ // delete old version of this keyRing, which also deletes all keys and userIds on cascade
+ Uri deleteUri = KeyRings.buildPublicKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
+
+ try {
+ context.getContentResolver().delete(deleteUri, null, null);
+ } catch (UnsupportedOperationException e) {
+ Log.e(Constants.TAG, "Key could not be deleted! Maybe we are creating a new one!", e);
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(KeyRings.MASTER_KEY_ID, masterKeyId);
+ values.put(KeyRings.KEY_RING_DATA, keyRing.getEncoded());
+
+ // insert new version of this keyRing
+ Uri uri = KeyRings.buildPublicKeyRingsUri();
+ Uri insertedUri = context.getContentResolver().insert(uri, values);
+ long keyRingRowId = Long.valueOf(insertedUri.getLastPathSegment());
+
+ // save all keys and userIds included in keyRing object in database
+ ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
+
+ int rank = 0;
+ for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
+ operations.add(buildPublicKeyOperations(context, keyRingRowId, key, rank));
+ ++rank;
+ }
+
+ int userIdRank = 0;
+ for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
+ operations.add(buildPublicUserIdOperations(context, keyRingRowId, userId, userIdRank));
+ ++userIdRank;
+ }
+
+ try {
+ context.getContentResolver().applyBatch(KeychainContract.CONTENT_AUTHORITY_INTERNAL,
+ operations);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "applyBatch failed!", e);
+ } catch (OperationApplicationException e) {
+ Log.e(Constants.TAG, "applyBatch failed!", e);
+ }
+ }
+
+ /**
+ * Saves PGPSecretKeyRing with its keys and userIds in DB
+ *
+ * @param context
+ * @param keyRing
+ * @return
+ * @throws IOException
+ * @throws GeneralException
+ */
+ @SuppressWarnings("unchecked")
+ public static void saveKeyRing(Context context, PGPSecretKeyRing keyRing) throws IOException {
+ PGPSecretKey masterKey = keyRing.getSecretKey();
+ long masterKeyId = masterKey.getKeyID();
+
+ // delete old version of this keyRing, which also deletes all keys and userIds on cascade
+ Uri deleteUri = KeyRings.buildSecretKeyRingsByMasterKeyIdUri(Long.toString(masterKeyId));
+
+ try {
+ context.getContentResolver().delete(deleteUri, null, null);
+ } catch (UnsupportedOperationException e) {
+ Log.e(Constants.TAG, "Key could not be deleted! Maybe we are creating a new one!", e);
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(KeyRings.MASTER_KEY_ID, masterKeyId);
+ values.put(KeyRings.KEY_RING_DATA, keyRing.getEncoded());
+
+ // insert new version of this keyRing
+ Uri uri = KeyRings.buildSecretKeyRingsUri();
+ Uri insertedUri = context.getContentResolver().insert(uri, values);
+ long keyRingRowId = Long.valueOf(insertedUri.getLastPathSegment());
+
+ // save all keys and userIds included in keyRing object in database
+ ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
+
+ int rank = 0;
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ operations.add(buildSecretKeyOperations(context, keyRingRowId, key, rank));
+ ++rank;
+ }
+
+ int userIdRank = 0;
+ for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
+ operations.add(buildSecretUserIdOperations(context, keyRingRowId, userId, userIdRank));
+ ++userIdRank;
+ }
+
+ try {
+ context.getContentResolver().applyBatch(KeychainContract.CONTENT_AUTHORITY_INTERNAL,
+ operations);
+ } catch (RemoteException e) {
+ Log.e(Constants.TAG, "applyBatch failed!", e);
+ } catch (OperationApplicationException e) {
+ Log.e(Constants.TAG, "applyBatch failed!", e);
+ }
+ }
+
+ /**
+ * Build ContentProviderOperation to add PGPPublicKey to database corresponding to a keyRing
+ *
+ * @param context
+ * @param keyRingRowId
+ * @param key
+ * @param rank
+ * @return
+ * @throws IOException
+ */
+ private static ContentProviderOperation buildPublicKeyOperations(Context context,
+ long keyRingRowId, PGPPublicKey key, int rank) throws IOException {
+ ContentValues values = new ContentValues();
+ values.put(Keys.KEY_ID, key.getKeyID());
+ values.put(Keys.IS_MASTER_KEY, key.isMasterKey());
+ values.put(Keys.ALGORITHM, key.getAlgorithm());
+ values.put(Keys.KEY_SIZE, key.getBitStrength());
+ values.put(Keys.CAN_SIGN, PgpHelper.isSigningKey(key));
+ values.put(Keys.CAN_ENCRYPT, PgpHelper.isEncryptionKey(key));
+ values.put(Keys.IS_REVOKED, key.isRevoked());
+ values.put(Keys.CREATION, PgpHelper.getCreationDate(key).getTime() / 1000);
+ Date expiryDate = PgpHelper.getExpiryDate(key);
+ if (expiryDate != null) {
+ values.put(Keys.EXPIRY, expiryDate.getTime() / 1000);
+ }
+ values.put(Keys.KEY_RING_ROW_ID, keyRingRowId);
+ values.put(Keys.KEY_DATA, key.getEncoded());
+ values.put(Keys.RANK, rank);
+
+ Uri uri = Keys.buildPublicKeysUri(Long.toString(keyRingRowId));
+
+ return ContentProviderOperation.newInsert(uri).withValues(values).build();
+ }
+
+ /**
+ * Build ContentProviderOperation to add PublicUserIds to database corresponding to a keyRing
+ *
+ * @param context
+ * @param keyRingRowId
+ * @param key
+ * @param rank
+ * @return
+ * @throws IOException
+ */
+ private static ContentProviderOperation buildPublicUserIdOperations(Context context,
+ long keyRingRowId, String userId, int rank) {
+ ContentValues values = new ContentValues();
+ values.put(UserIds.KEY_RING_ROW_ID, keyRingRowId);
+ values.put(UserIds.USER_ID, userId);
+ values.put(UserIds.RANK, rank);
+
+ Uri uri = UserIds.buildPublicUserIdsUri(Long.toString(keyRingRowId));
+
+ return ContentProviderOperation.newInsert(uri).withValues(values).build();
+ }
+
+ /**
+ * Build ContentProviderOperation to add PGPSecretKey to database corresponding to a keyRing
+ *
+ * @param context
+ * @param keyRingRowId
+ * @param key
+ * @param rank
+ * @return
+ * @throws IOException
+ */
+ private static ContentProviderOperation buildSecretKeyOperations(Context context,
+ long keyRingRowId, PGPSecretKey key, int rank) throws IOException {
+ ContentValues values = new ContentValues();
+ values.put(Keys.KEY_ID, key.getKeyID());
+ values.put(Keys.IS_MASTER_KEY, key.isMasterKey());
+ values.put(Keys.ALGORITHM, key.getPublicKey().getAlgorithm());
+ values.put(Keys.KEY_SIZE, key.getPublicKey().getBitStrength());
+ values.put(Keys.CAN_SIGN, PgpHelper.isSigningKey(key));
+ values.put(Keys.CAN_ENCRYPT, PgpHelper.isEncryptionKey(key));
+ values.put(Keys.IS_REVOKED, key.getPublicKey().isRevoked());
+ values.put(Keys.CREATION, PgpHelper.getCreationDate(key).getTime() / 1000);
+ Date expiryDate = PgpHelper.getExpiryDate(key);
+ if (expiryDate != null) {
+ values.put(Keys.EXPIRY, expiryDate.getTime() / 1000);
+ }
+ values.put(Keys.KEY_RING_ROW_ID, keyRingRowId);
+ values.put(Keys.KEY_DATA, key.getEncoded());
+ values.put(Keys.RANK, rank);
+
+ Uri uri = Keys.buildSecretKeysUri(Long.toString(keyRingRowId));
+
+ return ContentProviderOperation.newInsert(uri).withValues(values).build();
+ }
+
+ /**
+ * Build ContentProviderOperation to add SecretUserIds to database corresponding to a keyRing
+ *
+ * @param context
+ * @param keyRingRowId
+ * @param key
+ * @param rank
+ * @return
+ * @throws IOException
+ */
+ private static ContentProviderOperation buildSecretUserIdOperations(Context context,
+ long keyRingRowId, String userId, int rank) {
+ ContentValues values = new ContentValues();
+ values.put(UserIds.KEY_RING_ROW_ID, keyRingRowId);
+ values.put(UserIds.USER_ID, userId);
+ values.put(UserIds.RANK, rank);
+
+ Uri uri = UserIds.buildSecretUserIdsUri(Long.toString(keyRingRowId));
+
+ return ContentProviderOperation.newInsert(uri).withValues(values).build();
+ }
+
+ /**
+ * Private helper method
+ *
+ * @param context
+ * @param queryUri
+ * @return
+ */
+ private static ArrayList<Long> getKeyRingsMasterKeyIds(Context context, Uri queryUri) {
+ Cursor cursor = context.getContentResolver().query(queryUri,
+ new String[] { KeyRings.MASTER_KEY_ID }, null, null, null);
+
+ ArrayList<Long> masterKeyIds = new ArrayList<Long>();
+ if (cursor != null) {
+ int masterKeyIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
+ if (cursor.moveToFirst()) {
+ do {
+ masterKeyIds.add(cursor.getLong(masterKeyIdCol));
+ } while (cursor.moveToNext());
+ }
+ }
+
+ if (cursor != null) {
+ cursor.close();
+ }
+
+ return masterKeyIds;
+ }
+
+ /**
+ * Retrieves ids of all SecretKeyRings
+ *
+ * @param context
+ * @return
+ */
+ public static ArrayList<Long> getSecretKeyRingsMasterKeyIds(Context context) {
+ Uri queryUri = KeyRings.buildSecretKeyRingsUri();
+ return getKeyRingsMasterKeyIds(context, queryUri);
+ }
+
+ /**
+ * Retrieves ids of all PublicKeyRings
+ *
+ * @param context
+ * @return
+ */
+ public static ArrayList<Long> getPublicKeyRingsMasterKeyIds(Context context) {
+ Uri queryUri = KeyRings.buildPublicKeyRingsUri();
+ return getKeyRingsMasterKeyIds(context, queryUri);
+ }
+
+ public static void deletePublicKeyRing(Context context, long rowId) {
+ ContentResolver cr = context.getContentResolver();
+ cr.delete(KeyRings.buildPublicKeyRingsUri(Long.toString(rowId)), null, null);
+ }
+
+ public static void deleteSecretKeyRing(Context context, long rowId) {
+ ContentResolver cr = context.getContentResolver();
+ cr.delete(KeyRings.buildSecretKeyRingsUri(Long.toString(rowId)), null, null);
+ }
+
+ /**
+ * Get master key id of keyring by its row id
+ *
+ * @param context
+ * @param keyRingRowId
+ * @return
+ */
+ public static long getPublicMasterKeyId(Context context, long keyRingRowId) {
+ Uri queryUri = KeyRings.buildPublicKeyRingsUri(String.valueOf(keyRingRowId));
+ return getMasterKeyId(context, queryUri, keyRingRowId);
+ }
+
+ /**
+ * Get master key id of keyring by its row id
+ *
+ * @param context
+ * @param keyRingRowId
+ * @return
+ */
+ public static long getSecretMasterKeyId(Context context, long keyRingRowId) {
+ Uri queryUri = KeyRings.buildSecretKeyRingsUri(String.valueOf(keyRingRowId));
+ return getMasterKeyId(context, queryUri, keyRingRowId);
+ }
+
+ /**
+ * Private helper method to get master key id of keyring by its row id
+ *
+ * @param context
+ * @param queryUri
+ * @param keyRingRowId
+ * @return
+ */
+ private static long getMasterKeyId(Context context, Uri queryUri, long keyRingRowId) {
+ String[] projection = new String[] { KeyRings.MASTER_KEY_ID };
+
+ ContentResolver cr = context.getContentResolver();
+ Cursor cursor = cr.query(queryUri, projection, null, null, null);
+
+ long masterKeyId = -1;
+ if (cursor != null && cursor.moveToFirst()) {
+ int masterKeyIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
+
+ masterKeyId = cursor.getLong(masterKeyIdCol);
+ }
+
+ if (cursor != null) {
+ cursor.close();
+ }
+
+ return masterKeyId;
+ }
+
+ public static ArrayList<String> getPublicKeyRingsAsArmoredString(Context context,
+ long[] masterKeyIds) {
+ return getKeyRingsAsArmoredString(context, KeyRings.buildPublicKeyRingsUri(), masterKeyIds);
+ }
+
+ public static ArrayList<String> getSecretKeyRingsAsArmoredString(Context context,
+ long[] masterKeyIds) {
+ return getKeyRingsAsArmoredString(context, KeyRings.buildSecretKeyRingsUri(), masterKeyIds);
+ }
+
+ private static ArrayList<String> getKeyRingsAsArmoredString(Context context, Uri uri,
+ long[] masterKeyIds) {
+ ArrayList<String> output = new ArrayList<String>();
+
+ if (masterKeyIds != null && masterKeyIds.length > 0) {
+
+ Cursor cursor = getCursorWithSelectedKeyringMasterKeyIds(context, uri, masterKeyIds);
+
+ if (cursor != null) {
+ int masterIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
+ int dataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA);
+ if (cursor.moveToFirst()) {
+ do {
+ Log.d(Constants.TAG, "masterKeyId: " + cursor.getLong(masterIdCol));
+
+ // get actual keyring data blob and write it to ByteArrayOutputStream
+ try {
+ Object keyRing = null;
+ byte[] data = cursor.getBlob(dataCol);
+ if (data != null) {
+ keyRing = PgpConversionHelper.BytesToPGPKeyRing(data);
+ }
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ArmoredOutputStream aos = new ArmoredOutputStream(bos);
+ aos.setHeader("Version", PgpMain.getFullVersion(context));
+
+ if (keyRing instanceof PGPSecretKeyRing) {
+ aos.write(((PGPSecretKeyRing) keyRing).getEncoded());
+ } else if (keyRing instanceof PGPPublicKeyRing) {
+ aos.write(((PGPPublicKeyRing) keyRing).getEncoded());
+ }
+ aos.close();
+
+ String armoredKey = bos.toString("UTF-8");
+
+ Log.d(Constants.TAG, "armouredKey:" + armoredKey);
+
+ output.add(armoredKey);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "IOException", e);
+ }
+ } while (cursor.moveToNext());
+ }
+ }
+
+ if (cursor != null) {
+ cursor.close();
+ }
+
+ } else {
+ Log.e(Constants.TAG, "No master keys given!");
+ }
+
+ if (output.size() > 0) {
+ return output;
+ } else {
+ return null;
+ }
+ }
+
+ public static byte[] getPublicKeyRingsAsByteArray(Context context, long[] masterKeyIds) {
+ return getKeyRingsAsByteArray(context, KeyRings.buildPublicKeyRingsUri(), masterKeyIds);
+ }
+
+ public static byte[] getSecretKeyRingsAsByteArray(Context context, long[] masterKeyIds) {
+ return getKeyRingsAsByteArray(context, KeyRings.buildSecretKeyRingsUri(), masterKeyIds);
+ }
+
+ private static byte[] getKeyRingsAsByteArray(Context context, Uri uri, long[] masterKeyIds) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+
+ if (masterKeyIds != null && masterKeyIds.length > 0) {
+
+ Cursor cursor = getCursorWithSelectedKeyringMasterKeyIds(context, uri, masterKeyIds);
+
+ if (cursor != null) {
+ int masterIdCol = cursor.getColumnIndex(KeyRings.MASTER_KEY_ID);
+ int dataCol = cursor.getColumnIndex(KeyRings.KEY_RING_DATA);
+ if (cursor.moveToFirst()) {
+ do {
+ Log.d(Constants.TAG, "masterKeyId: " + cursor.getLong(masterIdCol));
+
+ // get actual keyring data blob and write it to ByteArrayOutputStream
+ try {
+ bos.write(cursor.getBlob(dataCol));
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "IOException", e);
+ }
+ } while (cursor.moveToNext());
+ }
+ }
+
+ if (cursor != null) {
+ cursor.close();
+ }
+
+ } else {
+ Log.e(Constants.TAG, "No master keys given!");
+ }
+
+ return bos.toByteArray();
+ }
+
+ private static Cursor getCursorWithSelectedKeyringMasterKeyIds(Context context, Uri baseUri,
+ long[] masterKeyIds) {
+ Cursor cursor = null;
+ if (masterKeyIds != null && masterKeyIds.length > 0) {
+
+ String inMasterKeyList = KeyRings.MASTER_KEY_ID + " IN (";
+ for (int i = 0; i < masterKeyIds.length; ++i) {
+ if (i != 0) {
+ inMasterKeyList += ", ";
+ }
+ inMasterKeyList += DatabaseUtils.sqlEscapeString("" + masterKeyIds[i]);
+ }
+ inMasterKeyList += ")";
+
+ cursor = context.getContentResolver().query(baseUri,
+ new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.KEY_RING_DATA },
+ inMasterKeyList, null, null);
+ }
+
+ return cursor;
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainApiService.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainApiService.aidl
new file mode 100644
index 000000000..ac8327b8e
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainApiService.aidl
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service;
+
+import org.sufficientlysecure.keychain.service.handler.IKeychainEncryptHandler;
+import org.sufficientlysecure.keychain.service.handler.IKeychainDecryptHandler;
+import org.sufficientlysecure.keychain.service.handler.IKeychainGetDecryptionKeyIdHandler;
+
+/**
+ * All methods are oneway, which means they are asynchronous and non-blocking.
+ * Results are returned into given Handler, which has to be implemented on client side.
+ */
+interface IKeychainApiService {
+
+ /**
+ * Encrypt
+ *
+ * Either inputBytes or inputUri is given, the other should be null.
+ *
+ * @param inputBytes
+ * Byte array you want to encrypt
+ * @param inputUri
+ * Blob in ContentProvider you want to encrypt
+ * @param useAsciiArmor
+ * Convert bytes to ascii armored text to guard against encoding problems
+ * @param compression
+ * Compression: 0x21070001: none, 1: Zip, 2: Zlib, 3: BZip2
+ * @param encryptionKeyIds
+ * Ids of public keys used for encryption
+ * @param symmetricEncryptionAlgorithm
+ * 7: AES-128, 8: AES-192, 9: AES-256, 4: Blowfish, 10: Twofish, 3: CAST5,
+ * 6: DES, 2: Triple DES, 1: IDEA
+ * @param handler
+ * Results are returned to this IKeychainEncryptDecryptHandler Handler
+ * to onSuccessEncrypt(in byte[] output), after successful encryption
+ */
+ oneway void encryptAsymmetric(in byte[] inputBytes, in String inputUri, in boolean useAsciiArmor,
+ in int compression, in long[] encryptionKeyIds, in int symmetricEncryptionAlgorithm,
+ in IKeychainEncryptHandler handler);
+
+ /**
+ * Same as encryptAsymmetric but using a passphrase for symmetric encryption
+ *
+ * @param encryptionPassphrase
+ * Passphrase for direct symmetric encryption using symmetricEncryptionAlgorithm
+ */
+ oneway void encryptSymmetric(in byte[] inputBytes, in String inputUri, in boolean useAsciiArmor,
+ in int compression, in String encryptionPassphrase, in int symmetricEncryptionAlgorithm,
+ in IKeychainEncryptHandler handler);
+
+ /**
+ * Encrypt and sign
+ *
+ * Either inputBytes or inputUri is given, the other should be null.
+ *
+ * @param inputBytes
+ * Byte array you want to encrypt
+ * @param inputUri
+ * Blob in ContentProvider you want to encrypt
+ * @param useAsciiArmor
+ * Convert bytes to ascii armored text to guard against encoding problems
+ * @param compression
+ * Compression: 0x21070001: none, 1: Zip, 2: Zlib, 3: BZip2
+ * @param encryptionKeyIds
+ * Ids of public keys used for encryption
+ * @param symmetricEncryptionAlgorithm
+ * 7: AES-128, 8: AES-192, 9: AES-256, 4: Blowfish, 10: Twofish, 3: CAST5,
+ * 6: DES, 2: Triple DES, 1: IDEA
+ * @param signatureKeyId
+ * Key id of key to sign with
+ * @param signatureHashAlgorithm
+ * 1: MD5, 3: RIPEMD-160, 2: SHA-1, 11: SHA-224, 8: SHA-256, 9: SHA-384,
+ * 10: SHA-512
+ * @param signatureForceV3
+ * Force V3 signatures
+ * @param signaturePassphrase
+ * Passphrase to unlock signature key
+ * @param handler
+ * Results are returned to this IKeychainEncryptDecryptHandler Handler
+ * to onSuccessEncrypt(in byte[] output), after successful encryption and signing
+ */
+ oneway void encryptAndSignAsymmetric(in byte[] inputBytes, in String inputUri,
+ in boolean useAsciiArmor, in int compression, in long[] encryptionKeyIds,
+ in int symmetricEncryptionAlgorithm, in long signatureKeyId, in int signatureHashAlgorithm,
+ in boolean signatureForceV3, in String signaturePassphrase,
+ in IKeychainEncryptHandler handler);
+
+ /**
+ * Same as encryptAndSignAsymmetric but using a passphrase for symmetric encryption
+ *
+ * @param encryptionPassphrase
+ * Passphrase for direct symmetric encryption using symmetricEncryptionAlgorithm
+ */
+ oneway void encryptAndSignSymmetric(in byte[] inputBytes, in String inputUri,
+ in boolean useAsciiArmor, in int compression, in String encryptionPassphrase,
+ in int symmetricEncryptionAlgorithm, in long signatureKeyId, in int signatureHashAlgorithm,
+ in boolean signatureForceV3, in String signaturePassphrase,
+ in IKeychainEncryptHandler handler);
+
+ /**
+ * Decrypts and verifies given input bytes. If no signature is present this method
+ * will only decrypt.
+ *
+ * @param inputBytes
+ * Byte array you want to decrypt and verify
+ * @param inputUri
+ * Blob in ContentProvider you want to decrypt and verify
+ * @param keyPassphrase
+ * Passphrase to unlock secret key for decryption.
+ * @param handler
+ * Handler where to return results to after successful encryption
+ */
+ oneway void decryptAndVerifyAsymmetric(in byte[] inputBytes, in String inputUri,
+ in String keyPassphrase, in IKeychainDecryptHandler handler);
+
+ /**
+ * Same as decryptAndVerifyAsymmetric but for symmetric decryption.
+ *
+ * @param encryptionPassphrase
+ * Passphrase to decrypt
+ */
+ oneway void decryptAndVerifySymmetric(in byte[] inputBytes, in String inputUri,
+ in String encryptionPassphrase, in IKeychainDecryptHandler handler);
+
+ /**
+ *
+ */
+ oneway void getDecryptionKeyId(in byte[] inputBytes, in String inputUri,
+ in IKeychainGetDecryptionKeyIdHandler handler);
+
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainKeyService.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainKeyService.aidl
new file mode 100644
index 000000000..ecea2b8ff
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/IKeychainKeyService.aidl
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service;
+
+import org.sufficientlysecure.keychain.service.handler.IKeychainGetKeyringsHandler;
+
+/**
+ * All methods are oneway, which means they are asynchronous and non-blocking.
+ * Results are returned into given Handler, which has to be implemented on client side.
+ */
+interface IKeychainKeyService {
+
+ oneway void getPublicKeyRings(in long[] masterKeyIds, in boolean asAsciiArmoredStringArray,
+ in IKeychainGetKeyringsHandler handler);
+
+ oneway void getSecretKeyRings(in long[] masterKeyIds, in boolean asAsciiArmoredStringArray,
+ in IKeychainGetKeyringsHandler handler);
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainApiService.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainApiService.java
new file mode 100644
index 000000000..7c70c3c68
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainApiService.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+
+import org.spongycastle.openpgp.PGPException;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.helper.PgpMain.PgpGeneralException;
+import org.sufficientlysecure.keychain.util.InputData;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+import org.sufficientlysecure.keychain.service.IKeychainApiService;
+import org.sufficientlysecure.keychain.service.handler.IKeychainDecryptHandler;
+import org.sufficientlysecure.keychain.service.handler.IKeychainEncryptHandler;
+import org.sufficientlysecure.keychain.service.handler.IKeychainGetDecryptionKeyIdHandler;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+public class KeychainApiService extends Service {
+ Context mContext;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mContext = this;
+ Log.d(Constants.TAG, "KeychainApiService, onCreate()");
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Log.d(Constants.TAG, "KeychainApiService, onDestroy()");
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ // private static void writeToOutputStream(InputStream is, OutputStream os) throws IOException {
+ // byte[] buffer = new byte[8];
+ // int len = 0;
+ // while ((len = is.read(buffer)) != -1) {
+ // os.write(buffer, 0, len);
+ // }
+ // }
+
+ private synchronized void encryptAndSignSafe(byte[] inputBytes, String inputUri,
+ boolean useAsciiArmor, int compression, long[] encryptionKeyIds,
+ String encryptionPassphrase, int symmetricEncryptionAlgorithm, long signatureKeyId,
+ int signatureHashAlgorithm, boolean signatureForceV3, String signaturePassphrase,
+ IKeychainEncryptHandler handler) throws RemoteException {
+
+ try {
+ // TODO use inputUri
+
+ // InputStream inStream = null;
+ // if (isBlob) {
+ // ContentResolver cr = getContentResolver();
+ // try {
+ // inStream = cr.openInputStream(Uri.parse(pArgs.getString(arg.BLOB.name())));
+ // } catch (Exception e) {
+ // Log.e(TAG, "... exception on opening blob", e);
+ // }
+ // } else {
+ // inStream = new ByteArrayInputStream(pArgs.getString(arg.MESSAGE.name()).getBytes());
+ // }
+ // InputData in = new InputData(inStream, 0); // XXX Size second param?
+
+ // build InputData and write into OutputStream
+ InputStream inputStream = new ByteArrayInputStream(inputBytes);
+ long inputLength = inputBytes.length;
+ InputData input = new InputData(inputStream, inputLength);
+
+ OutputStream output = new ByteArrayOutputStream();
+
+ PgpMain.encryptAndSign(mContext, null, input, output, useAsciiArmor, compression,
+ encryptionKeyIds, encryptionPassphrase, symmetricEncryptionAlgorithm,
+ signatureKeyId, signatureHashAlgorithm, signatureForceV3, signaturePassphrase);
+
+ output.close();
+
+ // if (isBlob) {
+ // ContentResolver cr = getContentResolver();
+ // try {
+ // OutputStream outStream = cr.openOutputStream(Uri.parse(pArgs.getString(arg.BLOB
+ // .name())));
+ // writeToOutputStream(new ByteArrayInputStream(out.toString().getBytes()), outStream);
+ // outStream.close();
+ // } catch (Exception e) {
+ // Log.e(TAG, "... exception on writing blob", e);
+ // }
+ // } else {
+ // pReturn.putString(ret.RESULT.name(), out.toString());
+ // }
+
+ byte[] outputBytes = ((ByteArrayOutputStream) output).toByteArray();
+
+ // return over handler on client side
+ handler.onSuccess(outputBytes, null);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "KeychainService, Exception!", e);
+
+ try {
+ handler.onException(getExceptionId(e), e.getMessage());
+ } catch (Exception t) {
+ Log.e(Constants.TAG, "Error returning exception to client", t);
+ }
+ }
+ }
+
+ private synchronized void decryptAndVerifySafe(byte[] inputBytes, String inputUri,
+ String passphrase, boolean assumeSymmetric, IKeychainDecryptHandler handler)
+ throws RemoteException {
+
+ try {
+ // build InputData and write into OutputStream
+ InputStream inputStream = new ByteArrayInputStream(inputBytes);
+ long inputLength = inputBytes.length;
+ InputData inputData = new InputData(inputStream, inputLength);
+
+ OutputStream outputStream = new ByteArrayOutputStream();
+
+ Bundle outputBundle = PgpMain.decryptAndVerify(mContext, null, inputData, outputStream,
+ passphrase, assumeSymmetric);
+
+ outputStream.close();
+
+ byte[] outputBytes = ((ByteArrayOutputStream) outputStream).toByteArray();
+
+ // get signature informations from bundle
+ boolean signature = outputBundle.getBoolean(KeychainIntentService.RESULT_SIGNATURE);
+ long signatureKeyId = outputBundle
+ .getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
+ String signatureUserId = outputBundle
+ .getString(KeychainIntentService.RESULT_SIGNATURE_USER_ID);
+ boolean signatureSuccess = outputBundle
+ .getBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS);
+ boolean signatureUnknown = outputBundle
+ .getBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN);
+
+ // return over handler on client side
+ handler.onSuccess(outputBytes, null, signature, signatureKeyId, signatureUserId,
+ signatureSuccess, signatureUnknown);
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "KeychainService, Exception!", e);
+
+ try {
+ handler.onException(getExceptionId(e), e.getMessage());
+ } catch (Exception t) {
+ Log.e(Constants.TAG, "Error returning exception to client", t);
+ }
+ }
+ }
+
+ private synchronized void getDecryptionKeySafe(byte[] inputBytes, String inputUri,
+ IKeychainGetDecryptionKeyIdHandler handler) {
+
+ // TODO: implement inputUri
+
+ try {
+ InputStream inputStream = new ByteArrayInputStream(inputBytes);
+
+ long secretKeyId = Id.key.none;
+ boolean symmetric;
+
+ try {
+ secretKeyId = PgpMain.getDecryptionKeyId(KeychainApiService.this, inputStream);
+ if (secretKeyId == Id.key.none) {
+ throw new PgpGeneralException(getString(R.string.error_noSecretKeyFound));
+ }
+ symmetric = false;
+ } catch (PgpMain.NoAsymmetricEncryptionException e) {
+ secretKeyId = Id.key.symmetric;
+ if (!PgpMain.hasSymmetricEncryption(KeychainApiService.this, inputStream)) {
+ throw new PgpGeneralException(getString(R.string.error_noKnownEncryptionFound));
+ }
+ symmetric = true;
+ }
+
+ handler.onSuccess(secretKeyId, symmetric);
+
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "KeychainService, Exception!", e);
+
+ try {
+ handler.onException(getExceptionId(e), e.getMessage());
+ } catch (Exception t) {
+ Log.e(Constants.TAG, "Error returning exception to client", t);
+ }
+ }
+ }
+
+ /**
+ * This is the implementation of the interface IKeychainService. All methods are oneway, meaning
+ * asynchronous and return to the client using IKeychainHandler.
+ *
+ * The real PGP code is located in PGPMain.
+ */
+ private final IKeychainApiService.Stub mBinder = new IKeychainApiService.Stub() {
+
+ @Override
+ public void encryptAsymmetric(byte[] inputBytes, String inputUri, boolean useAsciiArmor,
+ int compression, long[] encryptionKeyIds, int symmetricEncryptionAlgorithm,
+ IKeychainEncryptHandler handler) throws RemoteException {
+
+ encryptAndSignSafe(inputBytes, inputUri, useAsciiArmor, compression, encryptionKeyIds,
+ null, symmetricEncryptionAlgorithm, Id.key.none, 0, false, null, handler);
+ }
+
+ @Override
+ public void encryptSymmetric(byte[] inputBytes, String inputUri, boolean useAsciiArmor,
+ int compression, String encryptionPassphrase, int symmetricEncryptionAlgorithm,
+ IKeychainEncryptHandler handler) throws RemoteException {
+
+ encryptAndSignSafe(inputBytes, inputUri, useAsciiArmor, compression, null,
+ encryptionPassphrase, symmetricEncryptionAlgorithm, Id.key.none, 0, false,
+ null, handler);
+ }
+
+ @Override
+ public void encryptAndSignAsymmetric(byte[] inputBytes, String inputUri,
+ boolean useAsciiArmor, int compression, long[] encryptionKeyIds,
+ int symmetricEncryptionAlgorithm, long signatureKeyId, int signatureHashAlgorithm,
+ boolean signatureForceV3, String signaturePassphrase,
+ IKeychainEncryptHandler handler) throws RemoteException {
+
+ encryptAndSignSafe(inputBytes, inputUri, useAsciiArmor, compression, encryptionKeyIds,
+ null, symmetricEncryptionAlgorithm, signatureKeyId, signatureHashAlgorithm,
+ signatureForceV3, signaturePassphrase, handler);
+ }
+
+ @Override
+ public void encryptAndSignSymmetric(byte[] inputBytes, String inputUri,
+ boolean useAsciiArmor, int compression, String encryptionPassphrase,
+ int symmetricEncryptionAlgorithm, long signatureKeyId, int signatureHashAlgorithm,
+ boolean signatureForceV3, String signaturePassphrase,
+ IKeychainEncryptHandler handler) throws RemoteException {
+
+ encryptAndSignSafe(inputBytes, inputUri, useAsciiArmor, compression, null,
+ encryptionPassphrase, symmetricEncryptionAlgorithm, signatureKeyId,
+ signatureHashAlgorithm, signatureForceV3, signaturePassphrase, handler);
+ }
+
+ @Override
+ public void decryptAndVerifyAsymmetric(byte[] inputBytes, String inputUri,
+ String keyPassphrase, IKeychainDecryptHandler handler) throws RemoteException {
+
+ decryptAndVerifySafe(inputBytes, inputUri, keyPassphrase, false, handler);
+ }
+
+ @Override
+ public void decryptAndVerifySymmetric(byte[] inputBytes, String inputUri,
+ String encryptionPassphrase, IKeychainDecryptHandler handler)
+ throws RemoteException {
+
+ decryptAndVerifySafe(inputBytes, inputUri, encryptionPassphrase, true, handler);
+ }
+
+ @Override
+ public void getDecryptionKeyId(byte[] inputBytes, String inputUri,
+ IKeychainGetDecryptionKeyIdHandler handler) throws RemoteException {
+
+ getDecryptionKeySafe(inputBytes, inputUri, handler);
+ }
+
+ };
+
+ /**
+ * As we can not throw an exception through Android RPC, we assign identifiers to the exception
+ * types.
+ *
+ * @param e
+ * @return
+ */
+ private int getExceptionId(Exception e) {
+ if (e instanceof NoSuchProviderException) {
+ return 0;
+ } else if (e instanceof NoSuchAlgorithmException) {
+ return 1;
+ } else if (e instanceof SignatureException) {
+ return 2;
+ } else if (e instanceof IOException) {
+ return 3;
+ } else if (e instanceof PgpGeneralException) {
+ return 4;
+ } else if (e instanceof PGPException) {
+ return 5;
+ } else {
+ return -1;
+ }
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentService.java
new file mode 100644
index 000000000..e525fe96e
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentService.java
@@ -0,0 +1,884 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.helper.PgpConversionHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.helper.PgpMain.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.DataStream;
+import org.sufficientlysecure.keychain.util.HkpKeyServer;
+import org.sufficientlysecure.keychain.util.InputData;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.ProgressDialogUpdater;
+import org.sufficientlysecure.keychain.util.KeyServer.KeyInfo;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+
+
+/**
+ * This Service contains all important long lasting operations for APG. It receives Intents with
+ * data from the activities or other apps, queues these intents, executes them, and stops itself
+ * after doing them.
+ */
+public class KeychainIntentService extends IntentService implements ProgressDialogUpdater {
+
+ /* extras that can be given by intent */
+ public static final String EXTRA_MESSENGER = "messenger";
+ public static final String EXTRA_ACTION = "action";
+ public static final String EXTRA_DATA = "data";
+
+ /* possible EXTRA_ACTIONs */
+ public static final int ACTION_ENCRYPT_SIGN = 10;
+
+ public static final int ACTION_DECRYPT_VERIFY = 20;
+
+ public static final int ACTION_SAVE_KEYRING = 30;
+ public static final int ACTION_GENERATE_KEY = 31;
+ public static final int ACTION_GENERATE_DEFAULT_RSA_KEYS = 32;
+
+ public static final int ACTION_DELETE_FILE_SECURELY = 40;
+
+ public static final int ACTION_IMPORT_KEYRING = 50;
+ public static final int ACTION_EXPORT_KEYRING = 51;
+
+ public static final int ACTION_UPLOAD_KEYRING = 60;
+ public static final int ACTION_QUERY_KEYRING = 61;
+
+ public static final int ACTION_SIGN_KEYRING = 70;
+
+ /* keys for data bundle */
+
+ // encrypt, decrypt, import export
+ public static final String TARGET = "target";
+ // possible targets:
+ public static final int TARGET_BYTES = 1;
+ public static final int TARGET_FILE = 2;
+ public static final int TARGET_STREAM = 3;
+
+ // encrypt
+ public static final String ENCRYPT_SECRET_KEY_ID = "secretKeyId";
+ public static final String ENCRYPT_USE_ASCII_AMOR = "useAsciiAmor";
+ public static final String ENCRYPT_ENCRYPTION_KEYS_IDS = "encryptionKeysIds";
+ public static final String ENCRYPT_COMPRESSION_ID = "compressionId";
+ public static final String ENCRYPT_GENERATE_SIGNATURE = "generateSignature";
+ public static final String ENCRYPT_SIGN_ONLY = "signOnly";
+ public static final String ENCRYPT_MESSAGE_BYTES = "messageBytes";
+ public static final String ENCRYPT_INPUT_FILE = "inputFile";
+ public static final String ENCRYPT_OUTPUT_FILE = "outputFile";
+ public static final String ENCRYPT_PROVIDER_URI = "providerUri";
+
+ // decrypt/verify
+ public static final String DECRYPT_SIGNED_ONLY = "signedOnly";
+ public static final String DECRYPT_RETURN_BYTES = "returnBinary";
+ public static final String DECRYPT_CIPHERTEXT_BYTES = "ciphertextBytes";
+ public static final String DECRYPT_ASSUME_SYMMETRIC = "assumeSymmetric";
+ public static final String DECRYPT_LOOKUP_UNKNOWN_KEY = "lookupUnknownKey";
+
+ // save keyring
+ public static final String SAVE_KEYRING_NEW_PASSPHRASE = "newPassphrase";
+ public static final String SAVE_KEYRING_CURRENT_PASSPHRASE = "currentPassphrase";
+ public static final String SAVE_KEYRING_USER_IDS = "userIds";
+ public static final String SAVE_KEYRING_KEYS = "keys";
+ public static final String SAVE_KEYRING_KEYS_USAGES = "keysUsages";
+ public static final String SAVE_KEYRING_MASTER_KEY_ID = "masterKeyId";
+
+ // generate key
+ public static final String GENERATE_KEY_ALGORITHM = "algorithm";
+ public static final String GENERATE_KEY_KEY_SIZE = "keySize";
+ public static final String GENERATE_KEY_SYMMETRIC_PASSPHRASE = "passphrase";
+ public static final String GENERATE_KEY_MASTER_KEY = "masterKey";
+
+ // delete file securely
+ public static final String DELETE_FILE = "deleteFile";
+
+ // import key
+ public static final String IMPORT_INPUT_STREAM = "importInputStream";
+ public static final String IMPORT_FILENAME = "importFilename";
+ public static final String IMPORT_BYTES = "importBytes";
+ // public static final String IMPORT_KEY_TYPE = "importKeyType";
+
+ // export key
+ public static final String EXPORT_OUTPUT_STREAM = "exportOutputStream";
+ public static final String EXPORT_FILENAME = "exportFilename";
+ public static final String EXPORT_KEY_TYPE = "exportKeyType";
+ public static final String EXPORT_ALL = "exportAll";
+ public static final String EXPORT_KEY_RING_MASTER_KEY_ID = "exportKeyRingId";
+
+ // upload key
+ public static final String UPLOAD_KEY_SERVER = "uploadKeyServer";
+ public static final String UPLOAD_KEY_KEYRING_ROW_ID = "uploadKeyRingId";
+
+ // query key
+ public static final String QUERY_KEY_SERVER = "queryKeyServer";
+ public static final String QUERY_KEY_TYPE = "queryKeyType";
+ public static final String QUERY_KEY_STRING = "queryKeyString";
+ public static final String QUERY_KEY_ID = "queryKeyId";
+
+ // sign key
+ public static final String SIGN_KEY_MASTER_KEY_ID = "signKeyMasterKeyId";
+ public static final String SIGN_KEY_PUB_KEY_ID = "signKeyPubKeyId";
+
+ /*
+ * possible data keys as result send over messenger
+ */
+ // keys
+ public static final String RESULT_NEW_KEY = "newKey";
+ public static final String RESULT_NEW_KEY2 = "newKey2";
+
+ // encrypt
+ public static final String RESULT_SIGNATURE_BYTES = "signatureData";
+ public static final String RESULT_SIGNATURE_STRING = "signatureText";
+ public static final String RESULT_ENCRYPTED_STRING = "encryptedMessage";
+ public static final String RESULT_ENCRYPTED_BYTES = "encryptedData";
+ public static final String RESULT_URI = "resultUri";
+
+ // decrypt/verify
+ public static final String RESULT_DECRYPTED_STRING = "decryptedMessage";
+ public static final String RESULT_DECRYPTED_BYTES = "decryptedData";
+ public static final String RESULT_SIGNATURE = "signature";
+ public static final String RESULT_SIGNATURE_KEY_ID = "signatureKeyId";
+ public static final String RESULT_SIGNATURE_USER_ID = "signatureUserId";
+
+ public static final String RESULT_SIGNATURE_SUCCESS = "signatureSuccess";
+ public static final String RESULT_SIGNATURE_UNKNOWN = "signatureUnknown";
+ public static final String RESULT_SIGNATURE_LOOKUP_KEY = "lookupKey";
+
+ // import
+ public static final String RESULT_IMPORT_ADDED = "added";
+ public static final String RESULT_IMPORT_UPDATED = "updated";
+ public static final String RESULT_IMPORT_BAD = "bad";
+
+ // export
+ public static final String RESULT_EXPORT = "exported";
+
+ // query
+ public static final String RESULT_QUERY_KEY_KEY_DATA = "queryKeyKeyData";
+ public static final String RESULT_QUERY_KEY_SEARCH_RESULT = "queryKeySearchResult";
+
+ Messenger mMessenger;
+
+ public KeychainIntentService() {
+ super("ApgService");
+ }
+
+ /**
+ * The IntentService calls this method from the default worker thread with the intent that
+ * started the service. When this method returns, IntentService stops the service, as
+ * appropriate.
+ */
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ Bundle extras = intent.getExtras();
+ if (extras == null) {
+ Log.e(Constants.TAG, "Extras bundle is null!");
+ return;
+ }
+
+ if (!(extras.containsKey(EXTRA_MESSENGER) || extras.containsKey(EXTRA_DATA) || extras
+ .containsKey(EXTRA_ACTION))) {
+ Log.e(Constants.TAG,
+ "Extra bundle must contain a messenger, a data bundle, and an action!");
+ return;
+ }
+
+ mMessenger = (Messenger) extras.get(EXTRA_MESSENGER);
+ Bundle data = extras.getBundle(EXTRA_DATA);
+
+ OtherHelper.logDebugBundle(data, "EXTRA_DATA");
+
+ int action = extras.getInt(EXTRA_ACTION);
+
+ // execute action from extra bundle
+ switch (action) {
+ case ACTION_ENCRYPT_SIGN:
+
+ try {
+ /* Input */
+ int target = data.getInt(TARGET);
+
+ long secretKeyId = data.getLong(ENCRYPT_SECRET_KEY_ID);
+ String encryptionPassphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
+
+ boolean useAsciiArmor = data.getBoolean(ENCRYPT_USE_ASCII_AMOR);
+ long encryptionKeyIds[] = data.getLongArray(ENCRYPT_ENCRYPTION_KEYS_IDS);
+ int compressionId = data.getInt(ENCRYPT_COMPRESSION_ID);
+ boolean generateSignature = data.getBoolean(ENCRYPT_GENERATE_SIGNATURE);
+ boolean signOnly = data.getBoolean(ENCRYPT_SIGN_ONLY);
+
+ InputStream inStream = null;
+ long inLength = -1;
+ InputData inputData = null;
+ OutputStream outStream = null;
+ String streamFilename = null;
+ switch (target) {
+ case TARGET_BYTES: /* encrypting bytes directly */
+ byte[] bytes = data.getByteArray(ENCRYPT_MESSAGE_BYTES);
+
+ inStream = new ByteArrayInputStream(bytes);
+ inLength = bytes.length;
+
+ inputData = new InputData(inStream, inLength);
+ outStream = new ByteArrayOutputStream();
+
+ break;
+ case TARGET_FILE: /* encrypting file */
+ String inputFile = data.getString(ENCRYPT_INPUT_FILE);
+ String outputFile = data.getString(ENCRYPT_OUTPUT_FILE);
+
+ // check if storage is ready
+ if (!FileHelper.isStorageMounted(inputFile)
+ || !FileHelper.isStorageMounted(outputFile)) {
+ throw new PgpGeneralException(
+ getString(R.string.error_externalStorageNotReady));
+ }
+
+ inStream = new FileInputStream(inputFile);
+ File file = new File(inputFile);
+ inLength = file.length();
+ inputData = new InputData(inStream, inLength);
+
+ outStream = new FileOutputStream(outputFile);
+
+ break;
+
+ case TARGET_STREAM: /* Encrypting stream from content uri */
+ Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI);
+
+ // InputStream
+ InputStream in = getContentResolver().openInputStream(providerUri);
+ inLength = PgpMain.getLengthOfStream(in);
+ inputData = new InputData(in, inLength);
+
+ // OutputStream
+ try {
+ while (true) {
+ streamFilename = PgpMain.generateRandomFilename(32);
+ if (streamFilename == null) {
+ throw new PgpMain.PgpGeneralException(
+ "couldn't generate random file name");
+ }
+ openFileInput(streamFilename).close();
+ }
+ } catch (FileNotFoundException e) {
+ // found a name that isn't used yet
+ }
+ outStream = openFileOutput(streamFilename, Context.MODE_PRIVATE);
+
+ break;
+
+ default:
+ throw new PgpMain.PgpGeneralException("No target choosen!");
+
+ }
+
+ /* Operation */
+
+ if (generateSignature) {
+ Log.d(Constants.TAG, "generating signature...");
+ PgpMain.generateSignature(this, this, inputData, outStream, useAsciiArmor,
+ false, secretKeyId, PassphraseCacheService.getCachedPassphrase(this,
+ secretKeyId), Preferences.getPreferences(this)
+ .getDefaultHashAlgorithm(), Preferences.getPreferences(this)
+ .getForceV3Signatures());
+ } else if (signOnly) {
+ Log.d(Constants.TAG, "sign only...");
+ PgpMain.signText(this, this, inputData, outStream, secretKeyId,
+ PassphraseCacheService.getCachedPassphrase(this, secretKeyId),
+ Preferences.getPreferences(this).getDefaultHashAlgorithm(), Preferences
+ .getPreferences(this).getForceV3Signatures());
+ } else {
+ Log.d(Constants.TAG, "encrypt...");
+ PgpMain.encryptAndSign(this, this, inputData, outStream, useAsciiArmor,
+ compressionId, encryptionKeyIds, encryptionPassphrase, Preferences
+ .getPreferences(this).getDefaultEncryptionAlgorithm(),
+ secretKeyId,
+ Preferences.getPreferences(this).getDefaultHashAlgorithm(), Preferences
+ .getPreferences(this).getForceV3Signatures(),
+ PassphraseCacheService.getCachedPassphrase(this, secretKeyId));
+ }
+
+ outStream.close();
+
+ /* Output */
+
+ Bundle resultData = new Bundle();
+
+ switch (target) {
+ case TARGET_BYTES:
+ if (useAsciiArmor) {
+ String output = new String(
+ ((ByteArrayOutputStream) outStream).toByteArray());
+ if (generateSignature) {
+ resultData.putString(RESULT_SIGNATURE_STRING, output);
+ } else {
+ resultData.putString(RESULT_ENCRYPTED_STRING, output);
+ }
+ } else {
+ byte output[] = ((ByteArrayOutputStream) outStream).toByteArray();
+ if (generateSignature) {
+ resultData.putByteArray(RESULT_SIGNATURE_BYTES, output);
+ } else {
+ resultData.putByteArray(RESULT_ENCRYPTED_BYTES, output);
+ }
+ }
+
+ break;
+ case TARGET_FILE:
+ // nothing, file was written, just send okay
+
+ break;
+ case TARGET_STREAM:
+ String uri = DataStream.buildDataStreamUri(streamFilename).toString();
+ resultData.putString(RESULT_URI, uri);
+
+ break;
+ }
+
+ OtherHelper.logDebugBundle(resultData, "resultData");
+
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
+ } catch (Exception e) {
+ sendErrorToHandler(e);
+ }
+
+ break;
+
+ case ACTION_DECRYPT_VERIFY:
+ try {
+ /* Input */
+ int target = data.getInt(TARGET);
+
+ long secretKeyId = data.getLong(ENCRYPT_SECRET_KEY_ID);
+ byte[] bytes = data.getByteArray(DECRYPT_CIPHERTEXT_BYTES);
+ boolean signedOnly = data.getBoolean(DECRYPT_SIGNED_ONLY);
+ boolean returnBytes = data.getBoolean(DECRYPT_RETURN_BYTES);
+ boolean assumeSymmetricEncryption = data.getBoolean(DECRYPT_ASSUME_SYMMETRIC);
+
+ boolean lookupUnknownKey = data.getBoolean(DECRYPT_LOOKUP_UNKNOWN_KEY);
+
+ InputStream inStream = null;
+ long inLength = -1;
+ InputData inputData = null;
+ OutputStream outStream = null;
+ String streamFilename = null;
+ switch (target) {
+ case TARGET_BYTES: /* decrypting bytes directly */
+ inStream = new ByteArrayInputStream(bytes);
+ inLength = bytes.length;
+
+ inputData = new InputData(inStream, inLength);
+ outStream = new ByteArrayOutputStream();
+
+ break;
+
+ case TARGET_FILE: /* decrypting file */
+ String inputFile = data.getString(ENCRYPT_INPUT_FILE);
+ String outputFile = data.getString(ENCRYPT_OUTPUT_FILE);
+
+ // check if storage is ready
+ if (!FileHelper.isStorageMounted(inputFile)
+ || !FileHelper.isStorageMounted(outputFile)) {
+ throw new PgpGeneralException(
+ getString(R.string.error_externalStorageNotReady));
+ }
+
+ // InputStream
+ inLength = -1;
+ inStream = new FileInputStream(inputFile);
+ File file = new File(inputFile);
+ inLength = file.length();
+ inputData = new InputData(inStream, inLength);
+
+ // OutputStream
+ outStream = new FileOutputStream(outputFile);
+
+ break;
+
+ case TARGET_STREAM: /* decrypting stream from content uri */
+ Uri providerUri = (Uri) data.getParcelable(ENCRYPT_PROVIDER_URI);
+
+ // InputStream
+ InputStream in = getContentResolver().openInputStream(providerUri);
+ inLength = PgpMain.getLengthOfStream(in);
+ inputData = new InputData(in, inLength);
+
+ // OutputStream
+ try {
+ while (true) {
+ streamFilename = PgpMain.generateRandomFilename(32);
+ if (streamFilename == null) {
+ throw new PgpMain.PgpGeneralException(
+ "couldn't generate random file name");
+ }
+ openFileInput(streamFilename).close();
+ }
+ } catch (FileNotFoundException e) {
+ // found a name that isn't used yet
+ }
+ outStream = openFileOutput(streamFilename, Context.MODE_PRIVATE);
+
+ break;
+
+ default:
+ throw new PgpMain.PgpGeneralException("No target choosen!");
+
+ }
+
+ /* Operation */
+
+ Bundle resultData = new Bundle();
+
+ // verifyText and decrypt returning additional resultData values for the
+ // verification of signatures
+ if (signedOnly) {
+ resultData = PgpMain.verifyText(this, this, inputData, outStream,
+ lookupUnknownKey);
+ } else {
+ resultData = PgpMain.decryptAndVerify(this, this, inputData, outStream,
+ PassphraseCacheService.getCachedPassphrase(this, secretKeyId),
+ assumeSymmetricEncryption);
+ }
+
+ outStream.close();
+
+ /* Output */
+
+ switch (target) {
+ case TARGET_BYTES:
+ if (returnBytes) {
+ byte output[] = ((ByteArrayOutputStream) outStream).toByteArray();
+ resultData.putByteArray(RESULT_DECRYPTED_BYTES, output);
+ } else {
+ String output = new String(
+ ((ByteArrayOutputStream) outStream).toByteArray());
+ resultData.putString(RESULT_DECRYPTED_STRING, output);
+ }
+
+ break;
+ case TARGET_FILE:
+ // nothing, file was written, just send okay and verification bundle
+
+ break;
+ case TARGET_STREAM:
+ String uri = DataStream.buildDataStreamUri(streamFilename).toString();
+ resultData.putString(RESULT_URI, uri);
+
+ break;
+ }
+
+ OtherHelper.logDebugBundle(resultData, "resultData");
+
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
+ } catch (Exception e) {
+ sendErrorToHandler(e);
+ }
+
+ break;
+
+ case ACTION_SAVE_KEYRING:
+
+ try {
+ /* Input */
+ String oldPassPhrase = data.getString(SAVE_KEYRING_CURRENT_PASSPHRASE);
+ String newPassPhrase = data.getString(SAVE_KEYRING_NEW_PASSPHRASE);
+ if (newPassPhrase == null) {
+ newPassPhrase = oldPassPhrase;
+ }
+ ArrayList<String> userIds = data.getStringArrayList(SAVE_KEYRING_USER_IDS);
+ ArrayList<PGPSecretKey> keys = PgpConversionHelper.BytesToPGPSecretKeyList(data
+ .getByteArray(SAVE_KEYRING_KEYS));
+ ArrayList<Integer> keysUsages = data.getIntegerArrayList(SAVE_KEYRING_KEYS_USAGES);
+ long masterKeyId = data.getLong(SAVE_KEYRING_MASTER_KEY_ID);
+
+ /* Operation */
+ PgpMain.buildSecretKey(this, userIds, keys, keysUsages, masterKeyId, oldPassPhrase,
+ newPassPhrase, this);
+ PassphraseCacheService.addCachedPassphrase(this, masterKeyId, newPassPhrase);
+
+ /* Output */
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
+ } catch (Exception e) {
+ sendErrorToHandler(e);
+ }
+
+ break;
+
+ case ACTION_GENERATE_KEY:
+
+ try {
+ /* Input */
+ int algorithm = data.getInt(GENERATE_KEY_ALGORITHM);
+ String passphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
+ int keysize = data.getInt(GENERATE_KEY_KEY_SIZE);
+ PGPSecretKey masterKey = null;
+ if (data.containsKey(GENERATE_KEY_MASTER_KEY)) {
+ masterKey = PgpConversionHelper.BytesToPGPSecretKey(data
+ .getByteArray(GENERATE_KEY_MASTER_KEY));
+ }
+
+ /* Operation */
+ PGPSecretKeyRing newKeyRing = PgpMain.createKey(this, algorithm, keysize,
+ passphrase, masterKey);
+
+ /* Output */
+ Bundle resultData = new Bundle();
+ resultData.putByteArray(RESULT_NEW_KEY,
+ PgpConversionHelper.PGPSecretKeyRingToBytes(newKeyRing));
+
+ OtherHelper.logDebugBundle(resultData, "resultData");
+
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
+ } catch (Exception e) {
+ sendErrorToHandler(e);
+ }
+
+ break;
+
+ case ACTION_GENERATE_DEFAULT_RSA_KEYS:
+ // generate one RSA 2048 key for signing and one subkey for encrypting!
+ try {
+ /* Input */
+ String passphrase = data.getString(GENERATE_KEY_SYMMETRIC_PASSPHRASE);
+
+ /* Operation */
+ PGPSecretKeyRing masterKeyRing = PgpMain.createKey(this, Id.choice.algorithm.rsa,
+ 2048, passphrase, null);
+
+ PGPSecretKeyRing subKeyRing = PgpMain.createKey(this, Id.choice.algorithm.rsa,
+ 2048, passphrase, masterKeyRing.getSecretKey());
+
+ /* Output */
+ Bundle resultData = new Bundle();
+ resultData.putByteArray(RESULT_NEW_KEY,
+ PgpConversionHelper.PGPSecretKeyRingToBytes(masterKeyRing));
+ resultData.putByteArray(RESULT_NEW_KEY2,
+ PgpConversionHelper.PGPSecretKeyRingToBytes(subKeyRing));
+
+ OtherHelper.logDebugBundle(resultData, "resultData");
+
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
+ } catch (Exception e) {
+ sendErrorToHandler(e);
+ }
+
+ break;
+
+ case ACTION_DELETE_FILE_SECURELY:
+ try {
+ /* Input */
+ String deleteFile = data.getString(DELETE_FILE);
+
+ /* Operation */
+ try {
+ PgpMain.deleteFileSecurely(this, this, new File(deleteFile));
+ } catch (FileNotFoundException e) {
+ throw new PgpMain.PgpGeneralException(getString(R.string.error_fileNotFound,
+ deleteFile));
+ } catch (IOException e) {
+ throw new PgpMain.PgpGeneralException(getString(
+ R.string.error_fileDeleteFailed, deleteFile));
+ }
+
+ /* Output */
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
+ } catch (Exception e) {
+ sendErrorToHandler(e);
+ }
+
+ break;
+
+ case ACTION_IMPORT_KEYRING:
+ try {
+
+ /* Input */
+ int target = data.getInt(TARGET);
+
+ // int keyType = Id.type.public_key;
+ // if (data.containsKey(IMPORT_KEY_TYPE)) {
+ // keyType = data.getInt(IMPORT_KEY_TYPE);
+ // }
+
+ /* Operation */
+ InputStream inStream = null;
+ long inLength = -1;
+ InputData inputData = null;
+ switch (target) {
+ case TARGET_BYTES: /* import key from bytes directly */
+ byte[] bytes = data.getByteArray(IMPORT_BYTES);
+
+ inStream = new ByteArrayInputStream(bytes);
+ inLength = bytes.length;
+
+ inputData = new InputData(inStream, inLength);
+
+ break;
+ case TARGET_FILE: /* import key from file */
+ String inputFile = data.getString(IMPORT_FILENAME);
+
+ inStream = new FileInputStream(inputFile);
+ File file = new File(inputFile);
+ inLength = file.length();
+ inputData = new InputData(inStream, inLength);
+
+ break;
+
+ case TARGET_STREAM:
+ // TODO: not implemented
+ break;
+ }
+
+ Bundle resultData = new Bundle();
+ resultData = PgpMain.importKeyRings(this, inputData, this);
+
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
+ } catch (Exception e) {
+ sendErrorToHandler(e);
+ }
+
+ break;
+
+ case ACTION_EXPORT_KEYRING:
+ try {
+
+ /* Input */
+ int keyType = Id.type.public_key;
+ if (data.containsKey(EXPORT_KEY_TYPE)) {
+ keyType = data.getInt(EXPORT_KEY_TYPE);
+ }
+
+ String outputFile = data.getString(EXPORT_FILENAME);
+
+ boolean exportAll = data.getBoolean(EXPORT_ALL);
+ long keyRingMasterKeyId = -1;
+ if (!exportAll) {
+ keyRingMasterKeyId = data.getLong(EXPORT_KEY_RING_MASTER_KEY_ID);
+ }
+
+ /* Operation */
+
+ // check if storage is ready
+ if (!FileHelper.isStorageMounted(outputFile)) {
+ throw new PgpGeneralException(getString(R.string.error_externalStorageNotReady));
+ }
+
+ // OutputStream
+ FileOutputStream outStream = new FileOutputStream(outputFile);
+
+ ArrayList<Long> keyRingMasterKeyIds = new ArrayList<Long>();
+ if (exportAll) {
+ // get all key ring row ids based on export type
+
+ if (keyType == Id.type.public_key) {
+ keyRingMasterKeyIds = ProviderHelper.getPublicKeyRingsMasterKeyIds(this);
+ } else {
+ keyRingMasterKeyIds = ProviderHelper.getSecretKeyRingsMasterKeyIds(this);
+ }
+ } else {
+ keyRingMasterKeyIds.add(keyRingMasterKeyId);
+ }
+
+ Bundle resultData = new Bundle();
+ resultData = PgpMain.exportKeyRings(this, keyRingMasterKeyIds, keyType, outStream,
+ this);
+
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
+ } catch (Exception e) {
+ sendErrorToHandler(e);
+ }
+
+ break;
+
+ case ACTION_UPLOAD_KEYRING:
+ try {
+
+ /* Input */
+ int keyRingRowId = data.getInt(UPLOAD_KEY_KEYRING_ROW_ID);
+ String keyServer = data.getString(UPLOAD_KEY_SERVER);
+
+ /* Operation */
+ HkpKeyServer server = new HkpKeyServer(keyServer);
+
+ PGPPublicKeyRing keyring = ProviderHelper.getPGPPublicKeyRingByRowId(this,
+ keyRingRowId);
+ if (keyring != null) {
+ boolean uploaded = PgpMain.uploadKeyRingToServer(server,
+ (PGPPublicKeyRing) keyring);
+ if (!uploaded) {
+ throw new PgpGeneralException("Unable to export key to selected server");
+ }
+ }
+
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
+ } catch (Exception e) {
+ sendErrorToHandler(e);
+ }
+
+ break;
+
+ case ACTION_QUERY_KEYRING:
+ try {
+
+ /* Input */
+ int queryType = data.getInt(QUERY_KEY_TYPE);
+ String keyServer = data.getString(QUERY_KEY_SERVER);
+
+ String queryString = data.getString(QUERY_KEY_STRING);
+ long keyId = data.getLong(QUERY_KEY_ID);
+
+ /* Operation */
+ Bundle resultData = new Bundle();
+
+ HkpKeyServer server = new HkpKeyServer(keyServer);
+ if (queryType == Id.keyserver.search) {
+ ArrayList<KeyInfo> searchResult = server.search(queryString);
+
+ resultData.putParcelableArrayList(RESULT_QUERY_KEY_SEARCH_RESULT, searchResult);
+ } else if (queryType == Id.keyserver.get) {
+ String keyData = server.get(keyId);
+
+ resultData.putString(RESULT_QUERY_KEY_KEY_DATA, keyData);
+ }
+
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, resultData);
+ } catch (Exception e) {
+ sendErrorToHandler(e);
+ }
+
+ break;
+
+ case ACTION_SIGN_KEYRING:
+ try {
+
+ /* Input */
+ long masterKeyId = data.getLong(SIGN_KEY_MASTER_KEY_ID);
+ long pubKeyId = data.getLong(SIGN_KEY_PUB_KEY_ID);
+
+ /* Operation */
+ String signaturePassPhrase = PassphraseCacheService.getCachedPassphrase(this,
+ masterKeyId);
+
+ PGPPublicKeyRing signedPubKeyRing = PgpMain.signKey(this, masterKeyId, pubKeyId,
+ signaturePassPhrase);
+
+ // store the signed key in our local cache
+ int retval = PgpMain.storeKeyRingInCache(this, signedPubKeyRing);
+ if (retval != Id.return_value.ok && retval != Id.return_value.updated) {
+ throw new PgpGeneralException("Failed to store signed key in local cache");
+ }
+
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY);
+ } catch (Exception e) {
+ sendErrorToHandler(e);
+ }
+
+ break;
+
+ default:
+ break;
+ }
+
+ }
+
+ private void sendErrorToHandler(Exception e) {
+ Log.e(Constants.TAG, "ApgService Exception: ", e);
+ e.printStackTrace();
+
+ Bundle data = new Bundle();
+ data.putString(KeychainIntentServiceHandler.DATA_ERROR, e.getMessage());
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_EXCEPTION, null, data);
+ }
+
+ private void sendMessageToHandler(Integer arg1, Integer arg2, Bundle data) {
+ Message msg = Message.obtain();
+ msg.arg1 = arg1;
+ if (arg2 != null) {
+ msg.arg2 = arg2;
+ }
+ if (data != null) {
+ msg.setData(data);
+ }
+
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
+ } catch (NullPointerException e) {
+ Log.w(Constants.TAG, "Messenger is null!", e);
+ }
+ }
+
+ private void sendMessageToHandler(Integer arg1, Bundle data) {
+ sendMessageToHandler(arg1, null, data);
+ }
+
+ private void sendMessageToHandler(Integer arg1) {
+ sendMessageToHandler(arg1, null, null);
+ }
+
+ /**
+ * Set progress of ProgressDialog by sending message to handler on UI thread
+ */
+ public void setProgress(String message, int progress, int max) {
+ Log.d(Constants.TAG, "Send message by setProgress with progress=" + progress + ", max="
+ + max);
+
+ Bundle data = new Bundle();
+ if (message != null) {
+ data.putString(KeychainIntentServiceHandler.DATA_MESSAGE, message);
+ }
+ data.putInt(KeychainIntentServiceHandler.DATA_PROGRESS, progress);
+ data.putInt(KeychainIntentServiceHandler.DATA_PROGRESS_MAX, max);
+
+ sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_UPDATE_PROGRESS, null, data);
+ }
+
+ public void setProgress(int resourceId, int progress, int max) {
+ setProgress(getString(resourceId), progress, max);
+ }
+
+ public void setProgress(int progress, int max) {
+ setProgress(null, progress, max);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java
new file mode 100644
index 000000000..4e43f2b3e
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainIntentServiceHandler.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service;
+
+import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v4.app.FragmentActivity;
+import android.widget.Toast;
+
+public class KeychainIntentServiceHandler extends Handler {
+
+ // possible messages send from this service to handler on ui
+ public static final int MESSAGE_OKAY = 1;
+ public static final int MESSAGE_EXCEPTION = 2;
+ public static final int MESSAGE_UPDATE_PROGRESS = 3;
+
+ // possible data keys for messages
+ public static final String DATA_ERROR = "error";
+ public static final String DATA_PROGRESS = "progress";
+ public static final String DATA_PROGRESS_MAX = "max";
+ public static final String DATA_MESSAGE = "message";
+ public static final String DATA_MESSAGE_ID = "message_id";
+
+ Activity mActivity;
+ ProgressDialogFragment mProgressDialogFragment;
+
+ public KeychainIntentServiceHandler(Activity activity) {
+ this.mActivity = activity;
+ }
+
+ public KeychainIntentServiceHandler(Activity activity, ProgressDialogFragment progressDialogFragment) {
+ this.mActivity = activity;
+ this.mProgressDialogFragment = progressDialogFragment;
+ }
+
+ public KeychainIntentServiceHandler(Activity activity, int progressDialogMessageId, int progressDialogStyle) {
+ this.mActivity = activity;
+ this.mProgressDialogFragment = ProgressDialogFragment.newInstance(progressDialogMessageId,
+ progressDialogStyle);
+ }
+
+ public void showProgressDialog(FragmentActivity activity) {
+ mProgressDialogFragment.show(activity.getSupportFragmentManager(), "progressDialog");
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ Bundle data = message.getData();
+
+ switch (message.arg1) {
+ case MESSAGE_OKAY:
+ mProgressDialogFragment.dismiss();
+
+ break;
+
+ case MESSAGE_EXCEPTION:
+ mProgressDialogFragment.dismiss();
+
+ // show error from service
+ if (data.containsKey(DATA_ERROR)) {
+ Toast.makeText(mActivity,
+ mActivity.getString(R.string.errorMessage, data.getString(DATA_ERROR)),
+ Toast.LENGTH_SHORT).show();
+ }
+
+ break;
+
+ case MESSAGE_UPDATE_PROGRESS:
+ if (data.containsKey(DATA_PROGRESS) && data.containsKey(DATA_PROGRESS_MAX)) {
+
+ // update progress from service
+ if (data.containsKey(DATA_MESSAGE)) {
+ mProgressDialogFragment.setProgress(data.getString(DATA_MESSAGE),
+ data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX));
+ } else if (data.containsKey(DATA_MESSAGE_ID)) {
+ mProgressDialogFragment.setProgress(data.getInt(DATA_MESSAGE_ID),
+ data.getInt(DATA_PROGRESS), data.getInt(DATA_PROGRESS_MAX));
+ } else {
+ mProgressDialogFragment.setProgress(data.getInt(DATA_PROGRESS),
+ data.getInt(DATA_PROGRESS_MAX));
+ }
+ }
+
+ break;
+
+ default:
+ break;
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainKeyService.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainKeyService.java
new file mode 100644
index 000000000..c0be8040f
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/KeychainKeyService.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service;
+
+import java.util.ArrayList;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.service.IKeychainKeyService;
+import org.sufficientlysecure.keychain.service.handler.IKeychainGetKeyringsHandler;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+public class KeychainKeyService extends Service {
+ Context mContext;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mContext = this;
+ Log.d(Constants.TAG, "ApgKeyService, onCreate()");
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ Log.d(Constants.TAG, "ApgKeyService, onDestroy()");
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ /**
+ * Synchronized implementation of getPublicKeyRings
+ */
+ private synchronized void getPublicKeyRingsSafe(long[] masterKeyIds,
+ boolean asAsciiArmoredStringArray, IKeychainGetKeyringsHandler handler)
+ throws RemoteException {
+ if (asAsciiArmoredStringArray) {
+ ArrayList<String> output = ProviderHelper.getPublicKeyRingsAsArmoredString(mContext,
+ masterKeyIds);
+
+ handler.onSuccess(null, output);
+ } else {
+ byte[] outputBytes = ProviderHelper
+ .getPublicKeyRingsAsByteArray(mContext, masterKeyIds);
+ handler.onSuccess(outputBytes, null);
+ }
+ }
+
+ /**
+ * Synchronized implementation of getSecretKeyRings
+ */
+ private synchronized void getSecretKeyRingsSafe(long[] masterKeyIds,
+ boolean asAsciiArmoredStringArray, IKeychainGetKeyringsHandler handler)
+ throws RemoteException {
+ if (asAsciiArmoredStringArray) {
+ ArrayList<String> output = ProviderHelper.getSecretKeyRingsAsArmoredString(mContext,
+ masterKeyIds);
+
+ handler.onSuccess(null, output);
+ } else {
+ byte[] outputBytes = ProviderHelper
+ .getSecretKeyRingsAsByteArray(mContext, masterKeyIds);
+ handler.onSuccess(outputBytes, null);
+ }
+
+ }
+
+ /**
+ * This is the implementation of the interface IApgKeyService. All methods are oneway, meaning
+ * asynchronous and return to the client using handlers.
+ *
+ * The real PGP code is located in PGPMain.
+ */
+ private final IKeychainKeyService.Stub mBinder = new IKeychainKeyService.Stub() {
+
+ @Override
+ public void getPublicKeyRings(long[] masterKeyIds, boolean asAsciiArmoredStringArray,
+ IKeychainGetKeyringsHandler handler) throws RemoteException {
+ getPublicKeyRingsSafe(masterKeyIds, asAsciiArmoredStringArray, handler);
+ }
+
+ @Override
+ public void getSecretKeyRings(long[] masterKeyIds, boolean asAsciiArmoredStringArray,
+ IKeychainGetKeyringsHandler handler) throws RemoteException {
+ getSecretKeyRingsSafe(masterKeyIds, asAsciiArmoredStringArray, handler);
+ }
+
+ };
+
+ /**
+ * As we can not throw an exception through Android RPC, we assign identifiers to the exception
+ * types.
+ *
+ * @param e
+ * @return
+ */
+ // private int getExceptionId(Exception e) {
+ // if (e instanceof NoSuchProviderException) {
+ // return 0;
+ // } else if (e instanceof NoSuchAlgorithmException) {
+ // return 1;
+ // } else if (e instanceof SignatureException) {
+ // return 2;
+ // } else if (e instanceof IOException) {
+ // return 3;
+ // } else if (e instanceof ApgGeneralException) {
+ // return 4;
+ // } else if (e instanceof PGPException) {
+ // return 5;
+ // } else {
+ // return -1;
+ // }
+ // }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/PassphraseCacheService.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/PassphraseCacheService.java
new file mode 100644
index 000000000..eb1232769
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/PassphraseCacheService.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service;
+
+import java.util.Date;
+import java.util.HashMap;
+
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+
+public class PassphraseCacheService extends Service {
+ public static final String TAG = Constants.TAG + ": PassphraseCacheService";
+
+ public static final String BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE = Constants.INTENT_PREFIX
+ + "PASSPHRASE_CACHE_SERVICE";
+
+ public static final String EXTRA_TTL = "ttl";
+ public static final String EXTRA_KEY_ID = "keyId";
+ public static final String EXTRA_PASSPHRASE = "passphrase";
+
+ private static final int REQUEST_ID = 0;
+ private static final long DEFAULT_TTL = 15;
+
+ private BroadcastReceiver mIntentReceiver;
+
+ // This is static to be easily retrieved by getCachedPassphrase() without the need of callback
+ // functions
+ private static HashMap<Long, String> mPassphraseCache = new HashMap<Long, String>();
+
+ /**
+ * This caches a new passphrase by sending a new command to the service. An android service is
+ * only run once. Thus, when the service is already started, new commands just add new events to
+ * the alarm manager for new passphrases to let them timeout in the future.
+ *
+ * @param context
+ * @param keyId
+ * @param passphrase
+ */
+ public static void addCachedPassphrase(Context context, long keyId, String passphrase) {
+ Log.d(TAG, "cacheNewPassphrase() for " + keyId);
+
+ Intent intent = new Intent(context, PassphraseCacheService.class);
+ intent.putExtra(EXTRA_TTL, Preferences.getPreferences(context).getPassPhraseCacheTtl());
+ intent.putExtra(EXTRA_PASSPHRASE, passphrase);
+ intent.putExtra(EXTRA_KEY_ID, keyId);
+
+ context.startService(intent);
+ }
+
+ /**
+ * Gets a cached passphrase from memory
+ *
+ * @param context
+ * @param keyId
+ * @return
+ */
+ public static String getCachedPassphrase(Context context, long keyId) {
+ // try to get master key id which is used as an identifier for cached passphrases
+ long masterKeyId = keyId;
+ if (masterKeyId != Id.key.symmetric) {
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByKeyId(context, keyId);
+ if (keyRing == null) {
+ return null;
+ }
+ PGPSecretKey masterKey = PgpHelper.getMasterKey(keyRing);
+ if (masterKey == null) {
+ return null;
+ }
+ masterKeyId = masterKey.getKeyID();
+ }
+
+ // get cached passphrase
+ String cachedPassphrase = mPassphraseCache.get(masterKeyId);
+ if (cachedPassphrase == null) {
+ return null;
+ }
+ // set it again to reset the cache life cycle
+ Log.d(TAG, "Cache passphrase again when getting it!");
+ addCachedPassphrase(context, masterKeyId, cachedPassphrase);
+
+ return cachedPassphrase;
+ }
+
+ /**
+ * Register BroadcastReceiver that is unregistered when service is destroyed. This
+ * BroadcastReceiver hears on intents with ACTION_PASSPHRASE_CACHE_SERVICE to then timeout
+ * specific passphrases in memory.
+ */
+ private void registerReceiver() {
+ if (mIntentReceiver == null) {
+ mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ Log.d(TAG, "Received broadcast...");
+
+ if (action.equals(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE)) {
+ long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1);
+ timeout(context, keyId);
+ }
+ }
+ };
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE);
+ registerReceiver(mIntentReceiver, filter);
+ }
+ }
+
+ /**
+ * Build pending intent that is executed by alarm manager to time out a specific passphrase
+ *
+ * @param context
+ * @param keyId
+ * @return
+ */
+ private static PendingIntent buildIntent(Context context, long keyId) {
+ Intent intent = new Intent(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE);
+ intent.putExtra(EXTRA_KEY_ID, keyId);
+ PendingIntent sender = PendingIntent.getBroadcast(context, REQUEST_ID, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+
+ return sender;
+ }
+
+ @Override
+ public void onCreate() {
+ Log.d(TAG, "onCreate()");
+ }
+
+ /**
+ * Executed when service is started by intent
+ */
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.d(TAG, "onStartCommand()");
+
+ // register broadcastreceiver
+ registerReceiver();
+
+ if (intent != null) {
+ long ttl = intent.getLongExtra(EXTRA_TTL, DEFAULT_TTL);
+ long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1);
+ String passphrase = intent.getStringExtra(EXTRA_PASSPHRASE);
+
+ Log.d(TAG, "Received intent in onStartCommand() with keyId: " + keyId + ", ttl: " + ttl);
+
+ // add keyId and passphrase to memory
+ mPassphraseCache.put(keyId, passphrase);
+
+ // register new alarm with keyId for this passphrase
+ long triggerTime = new Date().getTime() + (ttl * 1000);
+ AlarmManager am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
+ am.set(AlarmManager.RTC_WAKEUP, triggerTime, buildIntent(this, keyId));
+ }
+
+ return START_STICKY;
+ }
+
+ /**
+ * Called when one specific passphrase for keyId timed out
+ *
+ * @param context
+ * @param keyId
+ */
+ private void timeout(Context context, long keyId) {
+ // remove passphrase corresponding to keyId from memory
+ mPassphraseCache.remove(keyId);
+
+ Log.d(TAG, "Timeout of " + keyId + ", removed from memory!");
+
+ // stop whole service if no cached passphrases remaining
+ if (mPassphraseCache.isEmpty()) {
+ Log.d(TAG, "No passphrases remaining in memory, stopping service!");
+ stopSelf();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.d(TAG, "onDestroy()");
+
+ unregisterReceiver(mIntentReceiver);
+ }
+
+ public class PassphraseCacheBinder extends Binder {
+ public PassphraseCacheService getService() {
+ return PassphraseCacheService.this;
+ }
+ }
+
+ private final IBinder mBinder = new PassphraseCacheBinder();
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainDecryptHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainDecryptHandler.aidl
new file mode 100644
index 000000000..31ead701d
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainDecryptHandler.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service.handler;
+
+interface IKeychainDecryptHandler {
+
+ oneway void onSuccess(in byte[] outputBytes, in String outputUri, in boolean signature,
+ in long signatureKeyId, in String signatureUserId, in boolean signatureSuccess,
+ in boolean signatureUnknown);
+
+
+ oneway void onException(in int exceptionNumber, in String message);
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainEncryptHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainEncryptHandler.aidl
new file mode 100644
index 000000000..5b21a0613
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainEncryptHandler.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service.handler;
+
+interface IKeychainEncryptHandler {
+ /**
+ * Either output or streamUri is given. One of them is null
+ *
+ */
+ oneway void onSuccess(in byte[] outputBytes, in String outputUri);
+
+
+ oneway void onException(in int exceptionNumber, in String message);
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetDecryptionKeyIdHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetDecryptionKeyIdHandler.aidl
new file mode 100644
index 000000000..2fff74a92
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetDecryptionKeyIdHandler.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service.handler;
+
+interface IKeychainGetDecryptionKeyIdHandler {
+
+ oneway void onSuccess(in long secretKeyId, in boolean symmetric);
+
+
+ oneway void onException(in int exceptionNumber, in String message);
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetKeyringsHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetKeyringsHandler.aidl
new file mode 100644
index 000000000..c3a7d1faf
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainGetKeyringsHandler.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service.handler;
+
+interface IKeychainGetKeyringsHandler {
+ /**
+ * Either outputBytes or outputString is given. One of them is null
+ *
+ */
+ oneway void onSuccess(in byte[] outputBytes, in List<String> outputString);
+
+
+ oneway void onException(in int exceptionNumber, in String message);
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainSignHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainSignHandler.aidl
new file mode 100644
index 000000000..69badab4c
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainSignHandler.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service.handler;
+
+interface IKeychainSignHandler {
+ /**
+ * Either output or streamUri is given. One of them is null
+ *
+ */
+ oneway void onSuccess(in byte[] outputBytes, in String outputUri);
+
+
+ oneway void onException(in int exceptionNumber, in String message);
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainVerifyHandler.aidl b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainVerifyHandler.aidl
new file mode 100644
index 000000000..aaa9a7f6a
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/handler/IKeychainVerifyHandler.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.service.handler;
+
+interface IKeychainVerifyHandler {
+
+ oneway void onSuccess(in boolean signature, in long signatureKeyId,
+ in String signatureUserId, in boolean signatureSuccess, in boolean signatureUnknown);
+
+
+ oneway void onException(in int exceptionNumber, in String message);
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java
new file mode 100644
index 000000000..87b1204a4
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java
@@ -0,0 +1,946 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.LookupUnknownKeyDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.animation.AnimationUtils;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ViewFlipper;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.regex.Matcher;
+
+public class DecryptActivity extends SherlockFragmentActivity {
+
+ /* Intents */
+ // without permission
+ public static final String ACTION_DECRYPT = Constants.INTENT_PREFIX + "DECRYPT";
+ public static final String ACTION_DECRYPT_FILE = Constants.INTENT_PREFIX + "DECRYPT_FILE";
+
+ // with permission
+ public static final String ACTION_DECRYPT_AND_RETURN = Constants.INTENT_PREFIX
+ + "DECRYPT_AND_RETURN";
+ public static final String ACTION_DECRYPT_STREAM_AND_RETURN = Constants.INTENT_PREFIX
+ + "DECRYPT_STREAM_AND_RETURN";
+
+ /* EXTRA keys for input */
+ public static final String EXTRA_TEXT = "text";
+ public static final String EXTRA_DATA = "data";
+ public static final String EXTRA_REPLY_TO = "replyTo";
+ public static final String EXTRA_SUBJECT = "subject";
+ public static final String EXTRA_BINARY = "binary";
+
+ private long mSignatureKeyId = 0;
+
+ private boolean mReturnResult = false;
+ private String mReplyTo = null;
+ private String mSubject = null;
+ private boolean mSignedOnly = false;
+ private boolean mAssumeSymmetricEncryption = false;
+
+ private EditText mMessage = null;
+ private LinearLayout mSignatureLayout = null;
+ private ImageView mSignatureStatusImage = null;
+ private TextView mUserId = null;
+ private TextView mUserIdRest = null;
+
+ private ViewFlipper mSource = null;
+ private TextView mSourceLabel = null;
+ private ImageView mSourcePrevious = null;
+ private ImageView mSourceNext = null;
+
+ private boolean mDecryptEnabled = true;
+ private String mDecryptString = "";
+ private boolean mReplyEnabled = true;
+ private String mReplyString = "";
+
+ private int mDecryptTarget;
+
+ private EditText mFilename = null;
+ private CheckBox mDeleteAfter = null;
+ private ImageButton mBrowse = null;
+
+ private String mInputFilename = null;
+ private String mOutputFilename = null;
+
+ private Uri mContentUri = null;
+ private byte[] mDataBytes = null;
+ private boolean mReturnBinary = false;
+
+ private long mUnknownSignatureKeyId = 0;
+
+ private long mSecretKeyId = Id.key.none;
+
+ private FileDialogFragment mFileDialog;
+
+ private boolean mLookupUnknownKey = true;
+
+ private boolean mDecryptImmediately = false;
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+
+ if (mDecryptEnabled) {
+ menu.add(1, Id.menu.option.decrypt, 0, mDecryptString).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+ if (mReplyEnabled) {
+ menu.add(1, Id.menu.option.reply, 1, mReplyString).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.decrypt: {
+ decryptClicked();
+
+ return true;
+ }
+ case Id.menu.option.reply: {
+ replyClicked();
+
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ private void initView() {
+ mSource = (ViewFlipper) findViewById(R.id.source);
+ mSourceLabel = (TextView) findViewById(R.id.sourceLabel);
+ mSourcePrevious = (ImageView) findViewById(R.id.sourcePrevious);
+ mSourceNext = (ImageView) findViewById(R.id.sourceNext);
+
+ mSourcePrevious.setClickable(true);
+ mSourcePrevious.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_right_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_right_out));
+ mSource.showPrevious();
+ updateSource();
+ }
+ });
+
+ mSourceNext.setClickable(true);
+ OnClickListener nextSourceClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_left_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(DecryptActivity.this,
+ R.anim.push_left_out));
+ mSource.showNext();
+ updateSource();
+ }
+ };
+ mSourceNext.setOnClickListener(nextSourceClickListener);
+
+ mSourceLabel.setClickable(true);
+ mSourceLabel.setOnClickListener(nextSourceClickListener);
+
+ mMessage = (EditText) findViewById(R.id.message);
+ mSignatureLayout = (LinearLayout) findViewById(R.id.signature);
+ mSignatureStatusImage = (ImageView) findViewById(R.id.ic_signature_status);
+ mUserId = (TextView) findViewById(R.id.mainUserId);
+ mUserIdRest = (TextView) findViewById(R.id.mainUserIdRest);
+
+ // measure the height of the source_file view and set the message view's min height to that,
+ // so it fills mSource fully... bit of a hack.
+ View tmp = findViewById(R.id.sourceFile);
+ tmp.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ int height = tmp.getMeasuredHeight();
+ mMessage.setMinimumHeight(height);
+
+ mFilename = (EditText) findViewById(R.id.filename);
+ mBrowse = (ImageButton) findViewById(R.id.btn_browse);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ FileHelper.openFile(DecryptActivity.this, mFilename.getText().toString(), "*/*",
+ Id.request.filename);
+ }
+ });
+
+ mDeleteAfter = (CheckBox) findViewById(R.id.deleteAfterDecryption);
+
+ // default: message source
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceMessage) {
+ mSource.showNext();
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // check permissions for intent actions without user interaction
+ String[] restrictedActions = new String[] { ACTION_DECRYPT_AND_RETURN };
+ OtherHelper.checkPackagePermissionForActions(this, this.getCallingPackage(),
+ Constants.PERMISSION_ACCESS_API, getIntent().getAction(), restrictedActions);
+
+ setContentView(R.layout.decrypt);
+
+ // set actionbar without home button if called from another app
+ OtherHelper.setActionBarBackButton(this);
+
+ initView();
+
+ // Handle intent actions
+ handleActions(getIntent());
+
+ if (mSource.getCurrentView().getId() == R.id.sourceMessage
+ && mMessage.getText().length() == 0) {
+
+ CharSequence clipboardText = ClipboardReflection.getClipboardText(this);
+
+ String data = "";
+ if (clipboardText != null) {
+ Matcher matcher = PgpMain.PGP_MESSAGE.matcher(clipboardText);
+ if (!matcher.matches()) {
+ matcher = PgpMain.PGP_SIGNED_MESSAGE.matcher(clipboardText);
+ }
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ mMessage.setText(data);
+ Toast.makeText(this, R.string.usingClipboardContent, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ mSignatureLayout.setVisibility(View.GONE);
+ mSignatureLayout.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ if (mSignatureKeyId == 0) {
+ return;
+ }
+ PGPPublicKeyRing key = ProviderHelper.getPGPPublicKeyRingByKeyId(
+ DecryptActivity.this, mSignatureKeyId);
+ if (key != null) {
+ Intent intent = new Intent(DecryptActivity.this, KeyServerQueryActivity.class);
+ intent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID);
+ intent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, mSignatureKeyId);
+ startActivity(intent);
+ }
+ }
+ });
+
+ mReplyEnabled = false;
+
+ // build new actionbar
+ invalidateOptionsMenu();
+
+ if (mReturnResult) {
+ mSourcePrevious.setClickable(false);
+ mSourcePrevious.setEnabled(false);
+ mSourcePrevious.setVisibility(View.INVISIBLE);
+
+ mSourceNext.setClickable(false);
+ mSourceNext.setEnabled(false);
+ mSourceNext.setVisibility(View.INVISIBLE);
+
+ mSourceLabel.setClickable(false);
+ mSourceLabel.setEnabled(false);
+ }
+
+ updateSource();
+
+ if (mDecryptImmediately
+ || (mSource.getCurrentView().getId() == R.id.sourceMessage && (mMessage.getText()
+ .length() > 0 || mDataBytes != null || mContentUri != null))) {
+ decryptClicked();
+ }
+ }
+
+ /**
+ * Handles all actions with this intent
+ *
+ * @param intent
+ */
+ private void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+ String type = intent.getType();
+ Uri uri = intent.getData();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ /*
+ * Android's Action
+ */
+ if (Intent.ACTION_SEND.equals(action) && type != null) {
+ // When sending to APG Encrypt via share menu
+ if ("text/plain".equals(type)) {
+ // Plain text
+ String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
+ if (sharedText != null) {
+ // handle like normal text decryption, override action and extras to later
+ // execute ACTION_DECRYPT in main actions
+ extras.putString(EXTRA_TEXT, sharedText);
+ action = ACTION_DECRYPT;
+ }
+ } else {
+ // Binary via content provider (could also be files)
+ // override uri to get stream from send
+ uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ action = ACTION_DECRYPT_FILE;
+ }
+ } else if (Intent.ACTION_VIEW.equals(action)) {
+ // Android's Action when opening file associated to APG (see AndroidManifest.xml)
+
+ // override action
+ action = ACTION_DECRYPT_FILE;
+
+ // EVERYTHING ELSE IS OLD CODE
+ // This gets the Uri, where an inputStream can be opened from
+ // mContentUri = intent.getData();
+
+ // TODO: old implementation of ACTION_VIEW. Is this used in K9?
+ // Uri uri = mIntent.getData();
+ // try {
+ // InputStream attachment = getContentResolver().openInputStream(uri);
+ // ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+ // byte bytes[] = new byte[1 << 16];
+ // int length;
+ // while ((length = attachment.read(bytes)) > 0) {
+ // byteOut.write(bytes, 0, length);
+ // }
+ // byteOut.close();
+ // String data = new String(byteOut.toByteArray());
+ // mMessage.setText(data);
+ // } catch (FileNotFoundException e) {
+ // // ignore, then
+ // } catch (IOException e) {
+ // // ignore, then
+ // }
+
+ // same as ACTION_DECRYPT_FILE but decrypt it immediately
+ // handleActionDecryptFile(intent);
+ // mDecryptImmediately = true;
+ }
+
+ /**
+ * Main Actions
+ */
+ if (ACTION_DECRYPT.equals(action)) {
+ mDataBytes = extras.getByteArray(EXTRA_DATA);
+ String textData = null;
+ if (mDataBytes == null) {
+ Log.d(Constants.TAG, "EXTRA_DATA was null");
+ textData = extras.getString(EXTRA_TEXT);
+ } else {
+ Log.d(Constants.TAG, "Got data from EXTRA_DATA");
+ }
+ if (textData != null) {
+ Log.d(Constants.TAG, "textData null, matching text ...");
+ Matcher matcher = PgpMain.PGP_MESSAGE.matcher(textData);
+ if (matcher.matches()) {
+ Log.d(Constants.TAG, "PGP_MESSAGE matched");
+ textData = matcher.group(1);
+ // replace non breakable spaces
+ textData = textData.replaceAll("\\xa0", " ");
+ mMessage.setText(textData);
+ } else {
+ matcher = PgpMain.PGP_SIGNED_MESSAGE.matcher(textData);
+ if (matcher.matches()) {
+ Log.d(Constants.TAG, "PGP_SIGNED_MESSAGE matched");
+ textData = matcher.group(1);
+ // replace non breakable spaces
+ textData = textData.replaceAll("\\xa0", " ");
+ mMessage.setText(textData);
+
+ mDecryptString = getString(R.string.btn_verify);
+ // build new action bar
+ invalidateOptionsMenu();
+ } else {
+ Log.d(Constants.TAG, "Nothing matched!");
+ }
+ }
+ }
+ mReplyTo = extras.getString(EXTRA_REPLY_TO);
+ mSubject = extras.getString(EXTRA_SUBJECT);
+ } else if (ACTION_DECRYPT_FILE.equals(action)) {
+ // get file path from uri
+ String path = FileHelper.getPath(this, uri);
+
+ if (path != null) {
+ mInputFilename = path;
+ mFilename.setText(mInputFilename);
+ guessOutputFilename();
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceFile) {
+ mSource.showNext();
+ }
+ } else {
+ Log.e(Constants.TAG,
+ "Direct binary data without actual file in filesystem is not supported. This is only supported by ACTION_DECRYPT_STREAM_AND_RETURN.");
+ Toast.makeText(this, R.string.error_onlyFilesAreSupported, Toast.LENGTH_LONG)
+ .show();
+ // end activity
+ finish();
+ }
+ } else if (ACTION_DECRYPT_AND_RETURN.equals(action)) {
+ mReturnBinary = extras.getBoolean(EXTRA_BINARY, false);
+
+ if (mContentUri == null) {
+ mDataBytes = extras.getByteArray(EXTRA_DATA);
+ String data = extras.getString(EXTRA_TEXT);
+ if (data != null) {
+ Matcher matcher = PgpMain.PGP_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ // replace non breakable spaces
+ data = data.replaceAll("\\xa0", " ");
+ mMessage.setText(data);
+ } else {
+ matcher = PgpMain.PGP_SIGNED_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ // replace non breakable spaces
+ data = data.replaceAll("\\xa0", " ");
+ mMessage.setText(data);
+ mDecryptString = getString(R.string.btn_verify);
+
+ // build new action bar
+ invalidateOptionsMenu();
+ }
+ }
+ }
+ }
+ mReturnResult = true;
+ } else if (ACTION_DECRYPT_STREAM_AND_RETURN.equals(action)) {
+ // TODO: Implement decrypt stream
+ }
+ }
+
+ private void guessOutputFilename() {
+ mInputFilename = mFilename.getText().toString();
+ File file = new File(mInputFilename);
+ String filename = file.getName();
+ if (filename.endsWith(".asc") || filename.endsWith(".gpg") || filename.endsWith(".pgp")) {
+ filename = filename.substring(0, filename.length() - 4);
+ }
+ mOutputFilename = Constants.path.APP_DIR + "/" + filename;
+ }
+
+ private void updateSource() {
+ switch (mSource.getCurrentView().getId()) {
+ case R.id.sourceFile: {
+ mSourceLabel.setText(R.string.label_file);
+ mDecryptString = getString(R.string.btn_decrypt);
+
+ // build new action bar
+ invalidateOptionsMenu();
+ break;
+ }
+
+ case R.id.sourceMessage: {
+ mSourceLabel.setText(R.string.label_message);
+ mDecryptString = getString(R.string.btn_decrypt);
+
+ // build new action bar
+ invalidateOptionsMenu();
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+
+ private void decryptClicked() {
+ if (mSource.getCurrentView().getId() == R.id.sourceFile) {
+ mDecryptTarget = Id.target.file;
+ } else {
+ mDecryptTarget = Id.target.message;
+ }
+ initiateDecryption();
+ }
+
+ private void initiateDecryption() {
+ if (mDecryptTarget == Id.target.file) {
+ String currentFilename = mFilename.getText().toString();
+ if (mInputFilename == null || !mInputFilename.equals(currentFilename)) {
+ guessOutputFilename();
+ }
+
+ if (mInputFilename.equals("")) {
+ Toast.makeText(this, R.string.noFileSelected, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (mInputFilename.startsWith("file")) {
+ File file = new File(mInputFilename);
+ if (!file.exists() || !file.isFile()) {
+ Toast.makeText(
+ this,
+ getString(R.string.errorMessage, getString(R.string.error_fileNotFound)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+ }
+
+ if (mDecryptTarget == Id.target.message) {
+ String messageData = mMessage.getText().toString();
+ Matcher matcher = PgpMain.PGP_SIGNED_MESSAGE.matcher(messageData);
+ if (matcher.matches()) {
+ mSignedOnly = true;
+ decryptStart();
+ return;
+ }
+ }
+
+ // else treat it as an decrypted message/file
+ mSignedOnly = false;
+
+ getDecryptionKeyFromInputStream();
+
+ // if we need a symmetric passphrase or a passphrase to use a secret key ask for it
+ if (mSecretKeyId == Id.key.symmetric
+ || PassphraseCacheService.getCachedPassphrase(this, mSecretKeyId) == null) {
+ showPassphraseDialog();
+ } else {
+ if (mDecryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ decryptStart();
+ }
+ }
+ }
+
+ /**
+ * Shows passphrase dialog to cache a new passphrase the user enters for using it later for
+ * encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks
+ * for a symmetric passphrase
+ */
+ private void showPassphraseDialog() {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ if (mDecryptTarget == Id.target.file) {
+ askForOutputFilename();
+ } else {
+ decryptStart();
+ }
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ try {
+ PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
+ messenger, mSecretKeyId);
+
+ passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
+ } catch (PgpMain.PgpGeneralException e) {
+ Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
+ // send message to handler to start encryption directly
+ returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
+ }
+ }
+
+ /**
+ * TODO: Rework function, remove global variables
+ */
+ private void getDecryptionKeyFromInputStream() {
+ InputStream inStream = null;
+ if (mContentUri != null) {
+ try {
+ inStream = getContentResolver().openInputStream(mContentUri);
+ } catch (FileNotFoundException e) {
+ Log.e(Constants.TAG, "File not found!", e);
+ Toast.makeText(this, getString(R.string.error_fileNotFound, e.getMessage()),
+ Toast.LENGTH_SHORT).show();
+ }
+ } else if (mDecryptTarget == Id.target.file) {
+ // check if storage is ready
+ if (!FileHelper.isStorageMounted(mInputFilename)) {
+ Toast.makeText(this, getString(R.string.error_externalStorageNotReady),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ try {
+ inStream = new FileInputStream(mInputFilename);
+ } catch (FileNotFoundException e) {
+ Log.e(Constants.TAG, "File not found!", e);
+ Toast.makeText(this, getString(R.string.error_fileNotFound, e.getMessage()),
+ Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ if (mDataBytes != null) {
+ inStream = new ByteArrayInputStream(mDataBytes);
+ } else {
+ inStream = new ByteArrayInputStream(mMessage.getText().toString().getBytes());
+ }
+ }
+
+ // get decryption key for this inStream
+ try {
+ try {
+ mSecretKeyId = PgpMain.getDecryptionKeyId(this, inStream);
+ if (mSecretKeyId == Id.key.none) {
+ throw new PgpMain.PgpGeneralException(
+ getString(R.string.error_noSecretKeyFound));
+ }
+ mAssumeSymmetricEncryption = false;
+ } catch (PgpMain.NoAsymmetricEncryptionException e) {
+ mSecretKeyId = Id.key.symmetric;
+ if (!PgpMain.hasSymmetricEncryption(this, inStream)) {
+ throw new PgpMain.PgpGeneralException(
+ getString(R.string.error_noKnownEncryptionFound));
+ }
+ mAssumeSymmetricEncryption = true;
+ }
+ } catch (Exception e) {
+ Toast.makeText(this, getString(R.string.errorMessage, e.getMessage()),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void replyClicked() {
+ Intent intent = new Intent(this, EncryptActivity.class);
+ intent.setAction(EncryptActivity.ACTION_ENCRYPT);
+ String data = mMessage.getText().toString();
+ data = data.replaceAll("(?m)^", "> ");
+ data = "\n\n" + data;
+ intent.putExtra(EncryptActivity.EXTRA_TEXT, data);
+ intent.putExtra(EncryptActivity.EXTRA_SUBJECT, "Re: " + mSubject);
+ intent.putExtra(EncryptActivity.EXTRA_SEND_TO, mReplyTo);
+ intent.putExtra(EncryptActivity.EXTRA_SIGNATURE_KEY_ID, mSecretKeyId);
+ intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, new long[] { mSignatureKeyId });
+ startActivity(intent);
+ }
+
+ private void askForOutputFilename() {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == FileDialogFragment.MESSAGE_OKAY) {
+ Bundle data = message.getData();
+ mOutputFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
+ decryptStart();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ mFileDialog = FileDialogFragment.newInstance(messenger,
+ getString(R.string.title_decryptToFile),
+ getString(R.string.specifyFileToDecryptTo), mOutputFilename, null,
+ Id.request.output_filename);
+
+ mFileDialog.show(getSupportFragmentManager(), "fileDialog");
+ }
+
+ private void lookupUnknownKey(long unknownKeyId) {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == LookupUnknownKeyDialogFragment.MESSAGE_OKAY) {
+ // the result is handled by onActivityResult() as LookupUnknownKeyDialogFragment
+ // starts a new Intent which then returns data
+ } else if (message.what == LookupUnknownKeyDialogFragment.MESSAGE_CANCEL) {
+ // decrypt again, but don't lookup unknown keys!
+ mLookupUnknownKey = false;
+ decryptStart();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ LookupUnknownKeyDialogFragment lookupKeyDialog = LookupUnknownKeyDialogFragment
+ .newInstance(messenger, unknownKeyId);
+
+ lookupKeyDialog.show(getSupportFragmentManager(), "unknownKeyDialog");
+ }
+
+ private void decryptStart() {
+ Log.d(Constants.TAG, "decryptStart");
+
+ // Send all information needed to service to decrypt in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_DECRYPT_VERIFY);
+
+ // choose action based on input: decrypt stream, file or bytes
+ if (mContentUri != null) {
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_STREAM);
+
+ data.putParcelable(KeychainIntentService.ENCRYPT_PROVIDER_URI, mContentUri);
+ } else if (mDecryptTarget == Id.target.file) {
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE);
+
+ Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename="
+ + mOutputFilename);
+
+ data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename);
+ data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename);
+ } else {
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
+
+ if (mDataBytes != null) {
+ data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, mDataBytes);
+ } else {
+ String message = mMessage.getText().toString();
+ data.putByteArray(KeychainIntentService.DECRYPT_CIPHERTEXT_BYTES, message.getBytes());
+ }
+ }
+
+ data.putLong(KeychainIntentService.ENCRYPT_SECRET_KEY_ID, mSecretKeyId);
+
+ data.putBoolean(KeychainIntentService.DECRYPT_SIGNED_ONLY, mSignedOnly);
+ data.putBoolean(KeychainIntentService.DECRYPT_LOOKUP_UNKNOWN_KEY, mLookupUnknownKey);
+ data.putBoolean(KeychainIntentService.DECRYPT_RETURN_BYTES, mReturnBinary);
+ data.putBoolean(KeychainIntentService.DECRYPT_ASSUME_SYMMETRIC, mAssumeSymmetricEncryption);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after encrypting is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_decrypting, ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle returnData = message.getData();
+
+ // if key is unknown show lookup dialog
+ if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_LOOKUP_KEY)
+ && mLookupUnknownKey) {
+ mUnknownSignatureKeyId = returnData
+ .getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
+ lookupUnknownKey(mUnknownSignatureKeyId);
+ return;
+ }
+
+ mSignatureKeyId = 0;
+ mSignatureLayout.setVisibility(View.GONE);
+ mReplyEnabled = false;
+
+ // build new action bar
+ invalidateOptionsMenu();
+
+ Toast.makeText(DecryptActivity.this, R.string.decryptionSuccessful,
+ Toast.LENGTH_SHORT).show();
+ if (mReturnResult) {
+ Intent intent = new Intent();
+ intent.putExtras(returnData);
+ setResult(RESULT_OK, intent);
+ finish();
+ return;
+ }
+
+ switch (mDecryptTarget) {
+ case Id.target.message:
+ String decryptedMessage = returnData
+ .getString(KeychainIntentService.RESULT_DECRYPTED_STRING);
+ mMessage.setText(decryptedMessage);
+ mMessage.setHorizontallyScrolling(false);
+ mReplyEnabled = false;
+
+ // build new action bar
+ invalidateOptionsMenu();
+ break;
+
+ case Id.target.file:
+ if (mDeleteAfter.isChecked()) {
+ // Create and show dialog to delete original file
+ DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
+ .newInstance(mInputFilename);
+ deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog");
+ }
+ break;
+
+ default:
+ // shouldn't happen
+ break;
+
+ }
+
+ if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE)) {
+ String userId = returnData
+ .getString(KeychainIntentService.RESULT_SIGNATURE_USER_ID);
+ mSignatureKeyId = returnData
+ .getLong(KeychainIntentService.RESULT_SIGNATURE_KEY_ID);
+ mUserIdRest
+ .setText("id: " + PgpHelper.getSmallFingerPrint(mSignatureKeyId));
+ if (userId == null) {
+ userId = getResources().getString(R.string.unknownUserId);
+ }
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mUserIdRest.setText("<" + chunks[1]);
+ }
+ mUserId.setText(userId);
+
+ if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_SUCCESS)) {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_ok);
+ } else if (returnData.getBoolean(KeychainIntentService.RESULT_SIGNATURE_UNKNOWN)) {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ Toast.makeText(DecryptActivity.this,
+ R.string.unknownSignatureKeyTouchToLookUp, Toast.LENGTH_LONG)
+ .show();
+ } else {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ }
+ mSignatureLayout.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ try {
+ String path = FileHelper.getPath(this, data.getData());
+ Log.d(Constants.TAG, "path=" + path);
+
+ mFilename.setText(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!");
+ }
+ }
+ return;
+ }
+
+ case Id.request.output_filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ try {
+ String path = data.getData().getPath();
+ Log.d(Constants.TAG, "path=" + path);
+
+ mFileDialog.setFilename(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!");
+ }
+ }
+ return;
+ }
+
+ // this request is returned after LookupUnknownKeyDialogFragment started
+ // KeyServerQueryActivity and user looked uo key
+ case Id.request.look_up_key_id: {
+ Log.d(Constants.TAG, "Returning from Lookup Key...");
+ // decrypt again without lookup
+ mLookupUnknownKey = false;
+ decryptStart();
+ return;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java
new file mode 100644
index 000000000..73a1234c4
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java
@@ -0,0 +1,593 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.helper.PgpConversionHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.helper.PgpMain.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment;
+import org.sufficientlysecure.keychain.ui.widget.KeyEditor;
+import org.sufficientlysecure.keychain.ui.widget.SectionView;
+import org.sufficientlysecure.keychain.ui.widget.UserIdEditor;
+import org.sufficientlysecure.keychain.util.IterableIterator;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Vector;
+
+public class EditKeyActivity extends SherlockFragmentActivity {
+
+ // possible intent actions for this activity
+ public static final String ACTION_CREATE_KEY = Constants.INTENT_PREFIX + "CREATE_KEY";
+ public static final String ACTION_EDIT_KEY = Constants.INTENT_PREFIX + "EDIT_KEY";
+
+ // possible extra keys
+ public static final String EXTRA_USER_IDS = "userIds";
+ public static final String EXTRA_NO_PASSPHRASE = "noPassphrase";
+ public static final String EXTRA_GENERATE_DEFAULT_KEYS = "generateDefaultKeys";
+ public static final String EXTRA_KEY_ID = "keyId";
+
+ // results when saving key
+ public static final String RESULT_EXTRA_MASTER_KEY_ID = "masterKeyId";
+ public static final String RESULT_EXTRA_USER_ID = "userId";
+
+ private ActionBar mActionBar;
+
+ private PGPSecretKeyRing mKeyRing = null;
+
+ private SectionView mUserIdsView;
+ private SectionView mKeysView;
+
+ private String mCurrentPassPhrase = null;
+ private String mNewPassPhrase = null;
+
+ private Button mChangePassPhrase;
+
+ private CheckBox mNoPassphrase;
+
+ Vector<String> mUserIds;
+ Vector<PGPSecretKey> mKeys;
+ Vector<Integer> mKeysUsages;
+
+ // will be set to false to build layout later in handler
+ private boolean mBuildLayout = true;
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(1, Id.menu.option.cancel, 0, R.string.btn_doNotSave).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(1, Id.menu.option.save, 1, R.string.btn_save).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, KeyListSecretActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.save:
+ saveClicked();
+ return true;
+
+ case Id.menu.option.cancel:
+ cancelClicked();
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // check permissions for intent actions without user interaction
+ String[] restrictedActions = new String[] { ACTION_CREATE_KEY };
+ OtherHelper.checkPackagePermissionForActions(this, this.getCallingPackage(),
+ Constants.PERMISSION_ACCESS_API, getIntent().getAction(), restrictedActions);
+
+ setContentView(R.layout.edit_key);
+
+ mActionBar = getSupportActionBar();
+ mActionBar.setDisplayShowTitleEnabled(true);
+
+ // set actionbar without home button if called from another app
+ OtherHelper.setActionBarBackButton(this);
+
+ // find views
+ mChangePassPhrase = (Button) findViewById(R.id.edit_key_btn_change_pass_phrase);
+ mNoPassphrase = (CheckBox) findViewById(R.id.edit_key_no_passphrase);
+
+ mUserIds = new Vector<String>();
+ mKeys = new Vector<PGPSecretKey>();
+ mKeysUsages = new Vector<Integer>();
+
+ // Catch Intents opened from other apps
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ if (ACTION_CREATE_KEY.equals(action)) {
+ handleActionCreateKey(intent);
+ } else if (ACTION_EDIT_KEY.equals(action)) {
+ handleActionEditKey(intent);
+ }
+
+ mChangePassPhrase.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ showSetPassphraseDialog();
+ }
+ });
+
+ // disable passphrase when no passphrase checkobox is checked!
+ mNoPassphrase.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (isChecked) {
+ // remove passphrase
+ mNewPassPhrase = null;
+
+ mChangePassPhrase.setVisibility(View.GONE);
+ } else {
+ mChangePassPhrase.setVisibility(View.VISIBLE);
+ }
+ }
+ });
+
+ if (mBuildLayout) {
+ buildLayout();
+ }
+ }
+
+ /**
+ * Handle intent action to create new key
+ *
+ * @param intent
+ */
+ private void handleActionCreateKey(Intent intent) {
+ Bundle extras = intent.getExtras();
+
+ mActionBar.setTitle(R.string.title_createKey);
+
+ mCurrentPassPhrase = "";
+
+ if (extras != null) {
+ // if userId is given, prefill the fields
+ if (extras.containsKey(EXTRA_USER_IDS)) {
+ Log.d(Constants.TAG, "UserIds are given!");
+ mUserIds.add(extras.getString(EXTRA_USER_IDS));
+ }
+
+ // if no passphrase is given
+ if (extras.containsKey(EXTRA_NO_PASSPHRASE)) {
+ boolean noPassphrase = extras.getBoolean(EXTRA_NO_PASSPHRASE);
+ if (noPassphrase) {
+ // check "no passphrase" checkbox and remove button
+ mNoPassphrase.setChecked(true);
+ mChangePassPhrase.setVisibility(View.GONE);
+ }
+ }
+
+ // generate key
+ if (extras.containsKey(EXTRA_GENERATE_DEFAULT_KEYS)) {
+ boolean generateDefaultKeys = extras.getBoolean(EXTRA_GENERATE_DEFAULT_KEYS);
+ if (generateDefaultKeys) {
+
+ // build layout in handler after generating keys not directly in onCreate
+ mBuildLayout = false;
+
+ // Send all information needed to service generate keys in other thread
+ Intent serviceIntent = new Intent(this, KeychainIntentService.class);
+ serviceIntent.putExtra(KeychainIntentService.EXTRA_ACTION,
+ KeychainIntentService.ACTION_GENERATE_DEFAULT_RSA_KEYS);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+ data.putString(KeychainIntentService.GENERATE_KEY_SYMMETRIC_PASSPHRASE,
+ mCurrentPassPhrase);
+
+ serviceIntent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after generating is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_generating, ProgressDialog.STYLE_SPINNER) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get new key from data bundle returned from service
+ Bundle data = message.getData();
+ PGPSecretKeyRing masterKeyRing = (PGPSecretKeyRing) PgpConversionHelper
+ .BytesToPGPKeyRing(data
+ .getByteArray(KeychainIntentService.RESULT_NEW_KEY));
+ PGPSecretKeyRing subKeyRing = (PGPSecretKeyRing) PgpConversionHelper
+ .BytesToPGPKeyRing(data
+ .getByteArray(KeychainIntentService.RESULT_NEW_KEY2));
+
+ // add master key
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSecretKey> masterIt = masterKeyRing.getSecretKeys();
+ mKeys.add(masterIt.next());
+ mKeysUsages.add(Id.choice.usage.sign_only);
+
+ // add sub key
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSecretKey> subIt = subKeyRing.getSecretKeys();
+ subIt.next(); // masterkey
+ mKeys.add(subIt.next());
+ mKeysUsages.add(Id.choice.usage.encrypt_only);
+
+ buildLayout();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ serviceIntent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(serviceIntent);
+ }
+ }
+ }
+ }
+
+ /**
+ * Handle intent action to edit existing key
+ *
+ * @param intent
+ */
+ @SuppressWarnings("unchecked")
+ private void handleActionEditKey(Intent intent) {
+ Bundle extras = intent.getExtras();
+
+ mActionBar.setTitle(R.string.title_editKey);
+
+ mCurrentPassPhrase = PgpMain.getEditPassPhrase();
+ if (mCurrentPassPhrase == null) {
+ mCurrentPassPhrase = "";
+ }
+
+ if (mCurrentPassPhrase.equals("")) {
+ // check "no passphrase" checkbox and remove button
+ mNoPassphrase.setChecked(true);
+ mChangePassPhrase.setVisibility(View.GONE);
+ }
+
+ if (extras != null) {
+ if (extras.containsKey(EXTRA_KEY_ID)) {
+ long keyId = extras.getLong(EXTRA_KEY_ID);
+
+ if (keyId != 0) {
+ PGPSecretKey masterKey = null;
+ mKeyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this, keyId);
+ if (mKeyRing != null) {
+ masterKey = PgpHelper.getMasterKey(mKeyRing);
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(
+ mKeyRing.getSecretKeys())) {
+ mKeys.add(key);
+ mKeysUsages.add(-1); // get usage when view is created
+ }
+ }
+ if (masterKey != null) {
+ for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
+ Log.d(Constants.TAG, "Added userId " + userId);
+ mUserIds.add(userId);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Shows the dialog to set a new passphrase
+ */
+ private void showSetPassphraseDialog() {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == SetPassphraseDialogFragment.MESSAGE_OKAY) {
+ Bundle data = message.getData();
+
+ // set new returned passphrase!
+ mNewPassPhrase = data
+ .getString(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE);
+
+ updatePassPhraseButtonText();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ // set title based on isPassphraseSet()
+ int title = -1;
+ if (isPassphraseSet()) {
+ title = R.string.title_changePassPhrase;
+ } else {
+ title = R.string.title_setPassPhrase;
+ }
+
+ SetPassphraseDialogFragment setPassphraseDialog = SetPassphraseDialogFragment.newInstance(
+ messenger, title);
+
+ setPassphraseDialog.show(getSupportFragmentManager(), "setPassphraseDialog");
+ }
+
+ /**
+ * Build layout based on mUserId, mKeys and mKeysUsages Vectors. It creates Views for every user
+ * id and key.
+ */
+ private void buildLayout() {
+ // Build layout based on given userIds and keys
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ LinearLayout container = (LinearLayout) findViewById(R.id.edit_key_container);
+ mUserIdsView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
+ mUserIdsView.setType(Id.type.user_id);
+ mUserIdsView.setUserIds(mUserIds);
+ container.addView(mUserIdsView);
+ mKeysView = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
+ mKeysView.setType(Id.type.key);
+ mKeysView.setKeys(mKeys, mKeysUsages);
+ container.addView(mKeysView);
+
+ updatePassPhraseButtonText();
+ }
+
+ private long getMasterKeyId() {
+ if (mKeysView.getEditors().getChildCount() == 0) {
+ return 0;
+ }
+ return ((KeyEditor) mKeysView.getEditors().getChildAt(0)).getValue().getKeyID();
+ }
+
+ public boolean isPassphraseSet() {
+ if (mNoPassphrase.isChecked()) {
+ return true;
+ } else if ((!mCurrentPassPhrase.equals(""))
+ || (mNewPassPhrase != null && !mNewPassPhrase.equals(""))) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void saveClicked() {
+ try {
+ if (!isPassphraseSet()) {
+ throw new PgpMain.PgpGeneralException(this.getString(R.string.setAPassPhrase));
+ }
+
+ // Send all information needed to service to edit key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_SAVE_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+ data.putString(KeychainIntentService.SAVE_KEYRING_CURRENT_PASSPHRASE, mCurrentPassPhrase);
+ data.putString(KeychainIntentService.SAVE_KEYRING_NEW_PASSPHRASE, mNewPassPhrase);
+ data.putStringArrayList(KeychainIntentService.SAVE_KEYRING_USER_IDS,
+ getUserIds(mUserIdsView));
+ ArrayList<PGPSecretKey> keys = getKeys(mKeysView);
+ data.putByteArray(KeychainIntentService.SAVE_KEYRING_KEYS,
+ PgpConversionHelper.PGPSecretKeyArrayListToBytes(keys));
+ data.putIntegerArrayList(KeychainIntentService.SAVE_KEYRING_KEYS_USAGES,
+ getKeysUsages(mKeysView));
+ data.putLong(KeychainIntentService.SAVE_KEYRING_MASTER_KEY_ID, getMasterKeyId());
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after saving is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_saving, ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ Intent data = new Intent();
+ data.putExtra(RESULT_EXTRA_MASTER_KEY_ID, getMasterKeyId());
+ ArrayList<String> userIds = null;
+ try {
+ userIds = getUserIds(mUserIdsView);
+ } catch (PgpGeneralException e) {
+ Log.e(Constants.TAG, "exception while getting user ids", e);
+ }
+ data.putExtra(RESULT_EXTRA_USER_ID, userIds.get(0));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ } catch (PgpMain.PgpGeneralException e) {
+ Toast.makeText(this, getString(R.string.errorMessage, e.getMessage()),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void cancelClicked() {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ /**
+ * Returns user ids from the SectionView
+ *
+ * @param userIdsView
+ * @return
+ */
+ private ArrayList<String> getUserIds(SectionView userIdsView)
+ throws PgpMain.PgpGeneralException {
+ ArrayList<String> userIds = new ArrayList<String>();
+
+ ViewGroup userIdEditors = userIdsView.getEditors();
+
+ boolean gotMainUserId = false;
+ for (int i = 0; i < userIdEditors.getChildCount(); ++i) {
+ UserIdEditor editor = (UserIdEditor) userIdEditors.getChildAt(i);
+ String userId = null;
+ try {
+ userId = editor.getValue();
+ } catch (UserIdEditor.NoNameException e) {
+ throw new PgpMain.PgpGeneralException(
+ this.getString(R.string.error_userIdNeedsAName));
+ } catch (UserIdEditor.NoEmailException e) {
+ throw new PgpMain.PgpGeneralException(
+ this.getString(R.string.error_userIdNeedsAnEmailAddress));
+ } catch (UserIdEditor.InvalidEmailException e) {
+ throw new PgpMain.PgpGeneralException(e.getMessage());
+ }
+
+ if (userId.equals("")) {
+ continue;
+ }
+
+ if (editor.isMainUserId()) {
+ userIds.add(0, userId);
+ gotMainUserId = true;
+ } else {
+ userIds.add(userId);
+ }
+ }
+
+ if (userIds.size() == 0) {
+ throw new PgpMain.PgpGeneralException(getString(R.string.error_keyNeedsAUserId));
+ }
+
+ if (!gotMainUserId) {
+ throw new PgpMain.PgpGeneralException(
+ getString(R.string.error_mainUserIdMustNotBeEmpty));
+ }
+
+ return userIds;
+ }
+
+ /**
+ * Returns keys from the SectionView
+ *
+ * @param keysView
+ * @return
+ */
+ private ArrayList<PGPSecretKey> getKeys(SectionView keysView)
+ throws PgpMain.PgpGeneralException {
+ ArrayList<PGPSecretKey> keys = new ArrayList<PGPSecretKey>();
+
+ ViewGroup keyEditors = keysView.getEditors();
+
+ if (keyEditors.getChildCount() == 0) {
+ throw new PgpMain.PgpGeneralException(getString(R.string.error_keyNeedsMasterKey));
+ }
+
+ for (int i = 0; i < keyEditors.getChildCount(); ++i) {
+ KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i);
+ keys.add(editor.getValue());
+ }
+
+ return keys;
+ }
+
+ /**
+ * Returns usage selections of keys from the SectionView
+ *
+ * @param keysView
+ * @return
+ */
+ private ArrayList<Integer> getKeysUsages(SectionView keysView)
+ throws PgpMain.PgpGeneralException {
+ ArrayList<Integer> getKeysUsages = new ArrayList<Integer>();
+
+ ViewGroup keyEditors = keysView.getEditors();
+
+ if (keyEditors.getChildCount() == 0) {
+ throw new PgpMain.PgpGeneralException(getString(R.string.error_keyNeedsMasterKey));
+ }
+
+ for (int i = 0; i < keyEditors.getChildCount(); ++i) {
+ KeyEditor editor = (KeyEditor) keyEditors.getChildAt(i);
+ getKeysUsages.add(editor.getUsage());
+ }
+
+ return getKeysUsages;
+ }
+
+ private void updatePassPhraseButtonText() {
+ mChangePassPhrase.setText(isPassphraseSet() ? R.string.btn_changePassPhrase
+ : R.string.btn_setPassPhrase);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java
new file mode 100644
index 000000000..bb61fb890
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java
@@ -0,0 +1,1105 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+import org.sufficientlysecure.keychain.util.Choice;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.animation.AnimationUtils;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ViewFlipper;
+
+import java.io.File;
+import java.util.Vector;
+
+public class EncryptActivity extends SherlockFragmentActivity {
+
+ /* Intents */
+ // without permission
+ public static final String ACTION_ENCRYPT = Constants.INTENT_PREFIX + "ENCRYPT";
+ public static final String ACTION_ENCRYPT_FILE = Constants.INTENT_PREFIX + "ENCRYPT_FILE";
+
+ // with permission
+ public static final String ACTION_ENCRYPT_AND_RETURN = Constants.INTENT_PREFIX
+ + "ENCRYPT_AND_RETURN";
+ public static final String ACTION_GENERATE_SIGNATURE_AND_RETURN = Constants.INTENT_PREFIX
+ + "GENERATE_SIGNATURE_AND_RETURN";
+ public static final String ACTION_ENCRYPT_STREAM_AND_RETURN = Constants.INTENT_PREFIX
+ + "ENCRYPT_STREAM_AND_RETURN";
+
+ /* EXTRA keys for input */
+ public static final String EXTRA_TEXT = "text";
+ public static final String EXTRA_DATA = "data";
+ public static final String EXTRA_ASCII_ARMOUR = "asciiArmour";
+ public static final String EXTRA_SEND_TO = "sendTo";
+ public static final String EXTRA_SUBJECT = "subject";
+ public static final String EXTRA_SIGNATURE_KEY_ID = "signatureKeyId";
+ public static final String EXTRA_ENCRYPTION_KEY_IDS = "encryptionKeyIds";
+
+ private String mSubject = null;
+ private String mSendTo = null;
+
+ private long mEncryptionKeyIds[] = null;
+
+ private boolean mEncryptImmediately = false;
+ private EditText mMessage = null;
+ private Button mSelectKeysButton = null;
+
+ private boolean mEncryptEnabled = false;
+ private String mEncryptString = "";
+ private boolean mEncryptToClipboardEnabled = false;
+ private String mEncryptToClipboardString = "";
+
+ private CheckBox mSign = null;
+ private TextView mMainUserId = null;
+ private TextView mMainUserIdRest = null;
+
+ private ViewFlipper mSource = null;
+ private TextView mSourceLabel = null;
+ private ImageView mSourcePrevious = null;
+ private ImageView mSourceNext = null;
+
+ private ViewFlipper mMode = null;
+ private TextView mModeLabel = null;
+ private ImageView mModePrevious = null;
+ private ImageView mModeNext = null;
+
+ private int mEncryptTarget;
+
+ private EditText mPassPhrase = null;
+ private EditText mPassPhraseAgain = null;
+ private CheckBox mAsciiArmour = null;
+ private Spinner mFileCompression = null;
+
+ private EditText mFilename = null;
+ private CheckBox mDeleteAfter = null;
+ private ImageButton mBrowse = null;
+
+ private String mInputFilename = null;
+ private String mOutputFilename = null;
+
+ private boolean mAsciiArmorDemand = false;
+ private boolean mOverrideAsciiArmour = false;
+ private Uri mStreamAndReturnUri = null;
+ private byte[] mData = null;
+
+ private boolean mGenerateSignature = false;
+
+ private long mSecretKeyId = Id.key.none;
+
+ private FileDialogFragment mFileDialog;
+
+ /**
+ * ActionBar menu is created based on class variables to change it at runtime
+ *
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (mEncryptToClipboardEnabled) {
+ menu.add(1, Id.menu.option.encrypt_to_clipboard, 0, mEncryptToClipboardString)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+ if (mEncryptEnabled) {
+ menu.add(1, Id.menu.option.encrypt, 1, mEncryptString).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.encrypt_to_clipboard:
+ encryptToClipboardClicked();
+
+ return true;
+
+ case Id.menu.option.encrypt:
+ encryptClicked();
+
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // check permissions for intent actions without user interaction
+ String[] restrictedActions = new String[] { ACTION_ENCRYPT_AND_RETURN,
+ ACTION_GENERATE_SIGNATURE_AND_RETURN, ACTION_ENCRYPT_STREAM_AND_RETURN };
+ OtherHelper.checkPackagePermissionForActions(this, this.getCallingPackage(),
+ Constants.PERMISSION_ACCESS_API, getIntent().getAction(), restrictedActions);
+
+ setContentView(R.layout.encrypt);
+
+ // set actionbar without home button if called from another app
+ OtherHelper.setActionBarBackButton(this);
+
+ initView();
+
+ // Handle intent actions
+ handleActions(getIntent());
+
+ updateView();
+ updateSource();
+ updateMode();
+
+ if (mEncryptImmediately) {
+ mSourcePrevious.setClickable(false);
+ mSourcePrevious.setEnabled(false);
+ mSourcePrevious.setVisibility(View.INVISIBLE);
+
+ mSourceNext.setClickable(false);
+ mSourceNext.setEnabled(false);
+ mSourceNext.setVisibility(View.INVISIBLE);
+
+ mSourceLabel.setClickable(false);
+ mSourceLabel.setEnabled(false);
+ }
+
+ updateActionBarButtons();
+
+ if (mEncryptImmediately
+ && (mMessage.getText().length() > 0 || mData != null)
+ && ((mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0) || mSecretKeyId != 0)) {
+ encryptClicked();
+ }
+ }
+
+ /**
+ * Handles all actions with this intent
+ *
+ * @param intent
+ */
+ private void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+ String type = intent.getType();
+ Uri uri = intent.getData();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ /*
+ * Android's Action
+ */
+ if (Intent.ACTION_SEND.equals(action) && type != null) {
+ // When sending to APG Encrypt via share menu
+ if ("text/plain".equals(type)) {
+ // Plain text
+ String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
+ if (sharedText != null) {
+ // handle like normal text encryption, override action and extras to later
+ // execute ACTION_ENCRYPT in main actions
+ extras.putString(EXTRA_TEXT, sharedText);
+ extras.putBoolean(EXTRA_ASCII_ARMOUR, true);
+ action = ACTION_ENCRYPT;
+ }
+ } else {
+ // Files via content provider, override uri and action
+ uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ action = ACTION_ENCRYPT_FILE;
+ }
+ }
+
+ if (ACTION_ENCRYPT_AND_RETURN.equals(action)
+ || ACTION_GENERATE_SIGNATURE_AND_RETURN.equals(action)) {
+ mEncryptImmediately = true;
+ }
+
+ if (ACTION_GENERATE_SIGNATURE_AND_RETURN.equals(action)) {
+ mGenerateSignature = true;
+ mOverrideAsciiArmour = true;
+ mAsciiArmorDemand = false;
+ }
+
+ if (extras.containsKey(EXTRA_ASCII_ARMOUR)) {
+ mAsciiArmorDemand = extras.getBoolean(EXTRA_ASCII_ARMOUR, true);
+ mOverrideAsciiArmour = true;
+ mAsciiArmour.setChecked(mAsciiArmorDemand);
+ }
+
+ mData = extras.getByteArray(EXTRA_DATA);
+ String textData = null;
+ if (mData == null) {
+ textData = extras.getString(EXTRA_TEXT);
+ }
+ mSendTo = extras.getString(EXTRA_SEND_TO);
+ mSubject = extras.getString(EXTRA_SUBJECT);
+ long signatureKeyId = extras.getLong(EXTRA_SIGNATURE_KEY_ID);
+ long[] encryptionKeyIds = extras.getLongArray(EXTRA_ENCRYPTION_KEY_IDS);
+
+ // preselect keys given by intent
+ preselectKeys(signatureKeyId, encryptionKeyIds);
+
+ /**
+ * Main Actions
+ */
+ if (ACTION_ENCRYPT.equals(action) || ACTION_ENCRYPT_AND_RETURN.equals(action)
+ || ACTION_GENERATE_SIGNATURE_AND_RETURN.equals(action)) {
+ if (textData != null) {
+ mMessage.setText(textData);
+ }
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceMessage) {
+ mSource.showNext();
+ }
+ } else if (ACTION_ENCRYPT_FILE.equals(action)) {
+ // get file path from uri
+ String path = FileHelper.getPath(this, uri);
+
+ if (path != null) {
+ mInputFilename = path;
+ mFilename.setText(mInputFilename);
+
+ mSource.setInAnimation(null);
+ mSource.setOutAnimation(null);
+ while (mSource.getCurrentView().getId() != R.id.sourceFile) {
+ mSource.showNext();
+ }
+ } else {
+ Log.e(Constants.TAG,
+ "Direct binary data without actual file in filesystem is not supported. This is only supported by ACTION_ENCRYPT_STREAM_AND_RETURN.");
+ Toast.makeText(this, R.string.error_onlyFilesAreSupported, Toast.LENGTH_LONG)
+ .show();
+ // end activity
+ finish();
+ }
+ } else if (ACTION_ENCRYPT_STREAM_AND_RETURN.equals(action)) {
+ // TODO: Set mStreamAndReturnUri that is used later to encrypt a stream!
+
+ mStreamAndReturnUri = uri;
+ }
+ }
+
+ /**
+ * If an Intent gives a signatureKeyId and/or encryptionKeyIds, preselect those!
+ *
+ * @param preselectedSignatureKeyId
+ * @param preselectedEncryptionKeyIds
+ */
+ private void preselectKeys(long preselectedSignatureKeyId, long[] preselectedEncryptionKeyIds) {
+ if (preselectedSignatureKeyId != 0) {
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this,
+ preselectedSignatureKeyId);
+ PGPSecretKey masterKey = null;
+ if (keyRing != null) {
+ masterKey = PgpHelper.getMasterKey(keyRing);
+ if (masterKey != null) {
+ Vector<PGPSecretKey> signKeys = PgpHelper.getUsableSigningKeys(keyRing);
+ if (signKeys.size() > 0) {
+ mSecretKeyId = masterKey.getKeyID();
+ }
+ }
+ }
+ }
+
+ if (preselectedEncryptionKeyIds != null) {
+ Vector<Long> goodIds = new Vector<Long>();
+ for (int i = 0; i < preselectedEncryptionKeyIds.length; ++i) {
+ PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(this,
+ preselectedEncryptionKeyIds[i]);
+ PGPPublicKey masterKey = null;
+ if (keyRing == null) {
+ continue;
+ }
+ masterKey = PgpHelper.getMasterKey(keyRing);
+ if (masterKey == null) {
+ continue;
+ }
+ Vector<PGPPublicKey> encryptKeys = PgpHelper.getUsableEncryptKeys(keyRing);
+ if (encryptKeys.size() == 0) {
+ continue;
+ }
+ goodIds.add(masterKey.getKeyID());
+ }
+ if (goodIds.size() > 0) {
+ mEncryptionKeyIds = new long[goodIds.size()];
+ for (int i = 0; i < goodIds.size(); ++i) {
+ mEncryptionKeyIds[i] = goodIds.get(i);
+ }
+ }
+ }
+ }
+
+ /**
+ * Guess output filename based on input path
+ *
+ * @param path
+ * @return Suggestion for output filename
+ */
+ private String guessOutputFilename(String path) {
+ // output in the same directory but with additional ending
+ File file = new File(path);
+ String ending = (mAsciiArmour.isChecked() ? ".asc" : ".gpg");
+ String outputFilename = file.getParent() + File.separator + file.getName() + ending;
+
+ return outputFilename;
+ }
+
+ private void updateSource() {
+ switch (mSource.getCurrentView().getId()) {
+ case R.id.sourceFile: {
+ mSourceLabel.setText(R.string.label_file);
+ break;
+ }
+
+ case R.id.sourceMessage: {
+ mSourceLabel.setText(R.string.label_message);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ updateActionBarButtons();
+ }
+
+ /**
+ * Set ActionBar buttons based on parameters
+ *
+ * @param encryptEnabled
+ * @param encryptStringRes
+ * @param encryptToClipboardEnabled
+ * @param encryptToClipboardStringRes
+ */
+ private void setActionbarButtons(boolean encryptEnabled, int encryptStringRes,
+ boolean encryptToClipboardEnabled, int encryptToClipboardStringRes) {
+ mEncryptEnabled = encryptEnabled;
+ if (encryptEnabled) {
+ mEncryptString = getString(encryptStringRes);
+ }
+ mEncryptToClipboardEnabled = encryptToClipboardEnabled;
+ if (encryptToClipboardEnabled) {
+ mEncryptToClipboardString = getString(encryptToClipboardStringRes);
+ }
+
+ // build new action bar based on these class variables
+ invalidateOptionsMenu();
+ }
+
+ /**
+ * Update ActionBar buttons based on current selection in view
+ */
+ private void updateActionBarButtons() {
+ switch (mSource.getCurrentView().getId()) {
+ case R.id.sourceFile: {
+ setActionbarButtons(true, R.string.btn_encryptFile, false, 0);
+
+ break;
+ }
+
+ case R.id.sourceMessage: {
+ mSourceLabel.setText(R.string.label_message);
+
+ if (mMode.getCurrentView().getId() == R.id.modeSymmetric) {
+ if (mEncryptImmediately) {
+ setActionbarButtons(true, R.string.btn_encrypt, false, 0);
+ } else {
+ setActionbarButtons(true, R.string.btn_encryptAndEmail, true,
+ R.string.btn_encryptToClipboard);
+ }
+ } else {
+ if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) {
+ if (mSecretKeyId == 0) {
+ setActionbarButtons(false, 0, false, 0);
+ } else {
+ if (mEncryptImmediately) {
+ setActionbarButtons(true, R.string.btn_sign, false, 0);
+ } else {
+ setActionbarButtons(true, R.string.btn_signAndEmail, true,
+ R.string.btn_signToClipboard);
+ }
+ }
+ } else {
+ if (mEncryptImmediately) {
+ setActionbarButtons(true, R.string.btn_encrypt, false, 0);
+ } else {
+ setActionbarButtons(true, R.string.btn_encryptAndEmail, true,
+ R.string.btn_encryptToClipboard);
+ }
+ }
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ }
+
+ private void updateMode() {
+ switch (mMode.getCurrentView().getId()) {
+ case R.id.modeAsymmetric: {
+ mModeLabel.setText(R.string.label_asymmetric);
+ break;
+ }
+
+ case R.id.modeSymmetric: {
+ mModeLabel.setText(R.string.label_symmetric);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ updateActionBarButtons();
+ }
+
+ private void encryptToClipboardClicked() {
+ mEncryptTarget = Id.target.clipboard;
+ initiateEncryption();
+ }
+
+ private void encryptClicked() {
+ Log.d(Constants.TAG, "encryptClicked invoked!");
+
+ if (mSource.getCurrentView().getId() == R.id.sourceFile) {
+ mEncryptTarget = Id.target.file;
+ } else {
+ mEncryptTarget = Id.target.email;
+ }
+ initiateEncryption();
+ }
+
+ private void initiateEncryption() {
+ if (mEncryptTarget == Id.target.file) {
+ String currentFilename = mFilename.getText().toString();
+ if (mInputFilename == null || !mInputFilename.equals(currentFilename)) {
+ mInputFilename = mFilename.getText().toString();
+ }
+
+ mOutputFilename = guessOutputFilename(mInputFilename);
+
+ if (mInputFilename.equals("")) {
+ Toast.makeText(this, R.string.noFileSelected, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!mInputFilename.startsWith("content")) {
+ File file = new File(mInputFilename);
+ if (!file.exists() || !file.isFile()) {
+ Toast.makeText(
+ this,
+ getString(R.string.errorMessage, getString(R.string.error_fileNotFound)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+ }
+
+ // symmetric encryption
+ if (mMode.getCurrentView().getId() == R.id.modeSymmetric) {
+ boolean gotPassPhrase = false;
+ String passPhrase = mPassPhrase.getText().toString();
+ String passPhraseAgain = mPassPhraseAgain.getText().toString();
+ if (!passPhrase.equals(passPhraseAgain)) {
+ Toast.makeText(this, R.string.passPhrasesDoNotMatch, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ gotPassPhrase = (passPhrase.length() != 0);
+ if (!gotPassPhrase) {
+ Toast.makeText(this, R.string.passPhraseMustNotBeEmpty, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ } else {
+ boolean encryptIt = (mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0);
+ // for now require at least one form of encryption for files
+ if (!encryptIt && mEncryptTarget == Id.target.file) {
+ Toast.makeText(this, R.string.selectEncryptionKey, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!encryptIt && mSecretKeyId == 0) {
+ Toast.makeText(this, R.string.selectEncryptionOrSignatureKey, Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ if (mSecretKeyId != 0
+ && PassphraseCacheService.getCachedPassphrase(this, mSecretKeyId) == null) {
+ showPassphraseDialog();
+
+ return;
+ }
+ }
+
+ if (mEncryptTarget == Id.target.file) {
+ showOutputFileDialog();
+ } else {
+ encryptStart();
+ }
+ }
+
+ /**
+ * Shows passphrase dialog to cache a new passphrase the user enters for using it later for
+ * encryption
+ */
+ private void showPassphraseDialog() {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ if (mEncryptTarget == Id.target.file) {
+ showOutputFileDialog();
+ } else {
+ encryptStart();
+ }
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ try {
+ PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(
+ EncryptActivity.this, messenger, mSecretKeyId);
+
+ passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
+ } catch (PgpMain.PgpGeneralException e) {
+ Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
+ // send message to handler to start encryption directly
+ returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
+ }
+ }
+
+ private void showOutputFileDialog() {
+ // Message is received after file is selected
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == FileDialogFragment.MESSAGE_OKAY) {
+ Bundle data = message.getData();
+ mOutputFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
+ encryptStart();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ mFileDialog = FileDialogFragment.newInstance(messenger,
+ getString(R.string.title_encryptToFile),
+ getString(R.string.specifyFileToEncryptTo), mOutputFilename, null,
+ Id.request.output_filename);
+
+ mFileDialog.show(getSupportFragmentManager(), "fileDialog");
+ }
+
+ private void encryptStart() {
+ // Send all information needed to service to edit key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ boolean useAsciiArmor = true;
+ long encryptionKeyIds[] = null;
+ int compressionId = 0;
+ boolean signOnly = false;
+
+ if (mMode.getCurrentView().getId() == R.id.modeSymmetric) {
+ Log.d(Constants.TAG, "Symmetric encryption enabled!");
+ String passPhrase = mPassPhrase.getText().toString();
+ if (passPhrase.length() == 0) {
+ passPhrase = null;
+ }
+
+ data.putString(KeychainIntentService.GENERATE_KEY_SYMMETRIC_PASSPHRASE, passPhrase);
+ } else {
+ encryptionKeyIds = mEncryptionKeyIds;
+ signOnly = (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0);
+ }
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_ENCRYPT_SIGN);
+
+ // choose default settings, target and data bundle by target
+ if (mStreamAndReturnUri != null) {
+ // mIntentDataUri is only defined when ACTION_ENCRYPT_STREAM_AND_RETURN is used
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_STREAM);
+ data.putParcelable(KeychainIntentService.ENCRYPT_PROVIDER_URI, mStreamAndReturnUri);
+
+ } else if (mEncryptTarget == Id.target.file) {
+ useAsciiArmor = mAsciiArmour.isChecked();
+ compressionId = ((Choice) mFileCompression.getSelectedItem()).getId();
+
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE);
+
+ Log.d(Constants.TAG, "mInputFilename=" + mInputFilename + ", mOutputFilename="
+ + mOutputFilename);
+
+ data.putString(KeychainIntentService.ENCRYPT_INPUT_FILE, mInputFilename);
+ data.putString(KeychainIntentService.ENCRYPT_OUTPUT_FILE, mOutputFilename);
+
+ } else {
+ useAsciiArmor = true;
+ compressionId = Preferences.getPreferences(this).getDefaultMessageCompression();
+
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
+
+ if (mData != null) {
+ data.putByteArray(KeychainIntentService.ENCRYPT_MESSAGE_BYTES, mData);
+ } else {
+ String message = mMessage.getText().toString();
+ if (signOnly && !mEncryptImmediately) {
+ fixBadCharactersForGmail(message);
+ }
+ data.putByteArray(KeychainIntentService.ENCRYPT_MESSAGE_BYTES, message.getBytes());
+ }
+ }
+
+ if (mOverrideAsciiArmour) {
+ useAsciiArmor = mAsciiArmorDemand;
+ }
+
+ data.putLong(KeychainIntentService.ENCRYPT_SECRET_KEY_ID, mSecretKeyId);
+ data.putBoolean(KeychainIntentService.ENCRYPT_USE_ASCII_AMOR, useAsciiArmor);
+ data.putLongArray(KeychainIntentService.ENCRYPT_ENCRYPTION_KEYS_IDS, encryptionKeyIds);
+ data.putInt(KeychainIntentService.ENCRYPT_COMPRESSION_ID, compressionId);
+ data.putBoolean(KeychainIntentService.ENCRYPT_GENERATE_SIGNATURE, mGenerateSignature);
+ data.putBoolean(KeychainIntentService.ENCRYPT_SIGN_ONLY, signOnly);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after encrypting is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_encrypting, ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle data = message.getData();
+
+ String output;
+ switch (mEncryptTarget) {
+ case Id.target.clipboard:
+ output = data.getString(KeychainIntentService.RESULT_ENCRYPTED_STRING);
+ Log.d(Constants.TAG, "output: " + output);
+ ClipboardReflection.copyToClipboard(EncryptActivity.this, output);
+ Toast.makeText(EncryptActivity.this,
+ R.string.encryptionToClipboardSuccessful, Toast.LENGTH_SHORT)
+ .show();
+ break;
+
+ case Id.target.email:
+ if (mEncryptImmediately) {
+ Intent intent = new Intent();
+ intent.putExtras(data);
+ setResult(RESULT_OK, intent);
+ finish();
+ return;
+ }
+
+ output = data.getString(KeychainIntentService.RESULT_ENCRYPTED_STRING);
+ Log.d(Constants.TAG, "output: " + output);
+
+ Intent emailIntent = new Intent(Intent.ACTION_SEND);
+ emailIntent.setType("text/plain; charset=utf-8");
+ emailIntent.putExtra(Intent.EXTRA_TEXT, output);
+ if (mSubject != null) {
+ emailIntent.putExtra(Intent.EXTRA_SUBJECT, mSubject);
+ }
+ if (mSendTo != null) {
+ emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] { mSendTo });
+ }
+ startActivity(Intent.createChooser(emailIntent,
+ getString(R.string.title_sendEmail)));
+ break;
+
+ case Id.target.file:
+ Toast.makeText(EncryptActivity.this, R.string.encryptionSuccessful,
+ Toast.LENGTH_SHORT).show();
+
+ if (mDeleteAfter.isChecked()) {
+ // Create and show dialog to delete original file
+ DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
+ .newInstance(mInputFilename);
+ deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog");
+ }
+ break;
+
+ default:
+ // shouldn't happen
+ break;
+
+ }
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+
+ /**
+ * Fixes bad message characters for gmail
+ *
+ * @param message
+ * @return
+ */
+ private String fixBadCharactersForGmail(String message) {
+ // fix the message a bit, trailing spaces and newlines break stuff,
+ // because GMail sends as HTML and such things fuck up the
+ // signature,
+ // TODO: things like "<" and ">" also fuck up the signature
+ message = message.replaceAll(" +\n", "\n");
+ message = message.replaceAll("\n\n+", "\n\n");
+ message = message.replaceFirst("^\n+", "");
+ // make sure there'll be exactly one newline at the end
+ message = message.replaceFirst("\n*$", "\n");
+
+ return message;
+ }
+
+ private void initView() {
+ mSource = (ViewFlipper) findViewById(R.id.source);
+ mSourceLabel = (TextView) findViewById(R.id.sourceLabel);
+ mSourcePrevious = (ImageView) findViewById(R.id.sourcePrevious);
+ mSourceNext = (ImageView) findViewById(R.id.sourceNext);
+
+ mSourcePrevious.setClickable(true);
+ mSourcePrevious.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_out));
+ mSource.showPrevious();
+ updateSource();
+ }
+ });
+
+ mSourceNext.setClickable(true);
+ OnClickListener nextSourceClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ mSource.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_in));
+ mSource.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_out));
+ mSource.showNext();
+ updateSource();
+ }
+ };
+ mSourceNext.setOnClickListener(nextSourceClickListener);
+
+ mSourceLabel.setClickable(true);
+ mSourceLabel.setOnClickListener(nextSourceClickListener);
+
+ mMode = (ViewFlipper) findViewById(R.id.mode);
+ mModeLabel = (TextView) findViewById(R.id.modeLabel);
+ mModePrevious = (ImageView) findViewById(R.id.modePrevious);
+ mModeNext = (ImageView) findViewById(R.id.modeNext);
+
+ mModePrevious.setClickable(true);
+ mModePrevious.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mMode.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_in));
+ mMode.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_right_out));
+ mMode.showPrevious();
+ updateMode();
+ }
+ });
+
+ OnClickListener nextModeClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ mMode.setInAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_in));
+ mMode.setOutAnimation(AnimationUtils.loadAnimation(EncryptActivity.this,
+ R.anim.push_left_out));
+ mMode.showNext();
+ updateMode();
+ }
+ };
+ mModeNext.setOnClickListener(nextModeClickListener);
+
+ mModeLabel.setClickable(true);
+ mModeLabel.setOnClickListener(nextModeClickListener);
+
+ mMessage = (EditText) findViewById(R.id.message);
+ mSelectKeysButton = (Button) findViewById(R.id.btn_selectEncryptKeys);
+ mSign = (CheckBox) findViewById(R.id.sign);
+ mMainUserId = (TextView) findViewById(R.id.mainUserId);
+ mMainUserIdRest = (TextView) findViewById(R.id.mainUserIdRest);
+
+ mPassPhrase = (EditText) findViewById(R.id.passPhrase);
+ mPassPhraseAgain = (EditText) findViewById(R.id.passPhraseAgain);
+
+ // measure the height of the source_file view and set the message view's min height to that,
+ // so it fills mSource fully... bit of a hack.
+ View tmp = findViewById(R.id.sourceFile);
+ tmp.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ int height = tmp.getMeasuredHeight();
+ mMessage.setMinimumHeight(height);
+
+ mFilename = (EditText) findViewById(R.id.filename);
+ mBrowse = (ImageButton) findViewById(R.id.btn_browse);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ FileHelper.openFile(EncryptActivity.this, mFilename.getText().toString(), "*/*",
+ Id.request.filename);
+ }
+ });
+
+ mFileCompression = (Spinner) findViewById(R.id.fileCompression);
+ Choice[] choices = new Choice[] {
+ new Choice(Id.choice.compression.none, getString(R.string.choice_none) + " ("
+ + getString(R.string.fast) + ")"),
+ new Choice(Id.choice.compression.zip, "ZIP (" + getString(R.string.fast) + ")"),
+ new Choice(Id.choice.compression.zlib, "ZLIB (" + getString(R.string.fast) + ")"),
+ new Choice(Id.choice.compression.bzip2, "BZIP2 (" + getString(R.string.very_slow)
+ + ")"), };
+ ArrayAdapter<Choice> adapter = new ArrayAdapter<Choice>(this,
+ android.R.layout.simple_spinner_item, choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mFileCompression.setAdapter(adapter);
+
+ int defaultFileCompression = Preferences.getPreferences(this).getDefaultFileCompression();
+ for (int i = 0; i < choices.length; ++i) {
+ if (choices[i].getId() == defaultFileCompression) {
+ mFileCompression.setSelection(i);
+ break;
+ }
+ }
+
+ mDeleteAfter = (CheckBox) findViewById(R.id.deleteAfterEncryption);
+
+ mAsciiArmour = (CheckBox) findViewById(R.id.asciiArmour);
+ mAsciiArmour.setChecked(Preferences.getPreferences(this).getDefaultAsciiArmour());
+
+ mSelectKeysButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ selectPublicKeys();
+ }
+ });
+
+ mSign.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ CheckBox checkBox = (CheckBox) v;
+ if (checkBox.isChecked()) {
+ selectSecretKey();
+ } else {
+ mSecretKeyId = Id.key.none;
+ updateView();
+ }
+ }
+ });
+ }
+
+ private void updateView() {
+ if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) {
+ mSelectKeysButton.setText(R.string.noKeysSelected);
+ } else if (mEncryptionKeyIds.length == 1) {
+ mSelectKeysButton.setText(R.string.oneKeySelected);
+ } else {
+ mSelectKeysButton.setText("" + mEncryptionKeyIds.length + " "
+ + getResources().getString(R.string.nKeysSelected));
+ }
+
+ if (mSecretKeyId == Id.key.none) {
+ mSign.setChecked(false);
+ mMainUserId.setText("");
+ mMainUserIdRest.setText("");
+ } else {
+ String uid = getResources().getString(R.string.unknownUserId);
+ String uidExtra = "";
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByMasterKeyId(this,
+ mSecretKeyId);
+ if (keyRing != null) {
+ PGPSecretKey key = PgpHelper.getMasterKey(keyRing);
+ if (key != null) {
+ String userId = PgpHelper.getMainUserIdSafe(this, key);
+ String chunks[] = userId.split(" <", 2);
+ uid = chunks[0];
+ if (chunks.length > 1) {
+ uidExtra = "<" + chunks[1];
+ }
+ }
+ }
+ mMainUserId.setText(uid);
+ mMainUserIdRest.setText(uidExtra);
+ mSign.setChecked(true);
+ }
+
+ updateActionBarButtons();
+ }
+
+ private void selectPublicKeys() {
+ Intent intent = new Intent(this, SelectPublicKeyActivity.class);
+ Vector<Long> keyIds = new Vector<Long>();
+ if (mSecretKeyId != 0) {
+ keyIds.add(mSecretKeyId);
+ }
+ if (mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0) {
+ for (int i = 0; i < mEncryptionKeyIds.length; ++i) {
+ keyIds.add(mEncryptionKeyIds[i]);
+ }
+ }
+ long[] initialKeyIds = null;
+ if (keyIds.size() > 0) {
+ initialKeyIds = new long[keyIds.size()];
+ for (int i = 0; i < keyIds.size(); ++i) {
+ initialKeyIds[i] = keyIds.get(i);
+ }
+ }
+ intent.putExtra(SelectPublicKeyActivity.EXTRA_SELECTED_MASTER_KEY_IDS, initialKeyIds);
+ startActivityForResult(intent, Id.request.public_keys);
+ }
+
+ private void selectSecretKey() {
+ Intent intent = new Intent(this, SelectSecretKeyActivity.class);
+ startActivityForResult(intent, Id.request.secret_keys);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ try {
+ String path = FileHelper.getPath(this, data.getData());
+ Log.d(Constants.TAG, "path=" + path);
+
+ mFilename.setText(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!");
+ }
+ }
+ return;
+ }
+
+ case Id.request.output_filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ try {
+ String path = data.getData().getPath();
+ Log.d(Constants.TAG, "path=" + path);
+
+ mFileDialog.setFilename(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!");
+ }
+ }
+ return;
+ }
+
+ case Id.request.public_keys: {
+ if (resultCode == RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ mEncryptionKeyIds = bundle
+ .getLongArray(SelectPublicKeyActivity.RESULT_EXTRA_MASTER_KEY_IDS);
+ }
+ updateView();
+ break;
+ }
+
+ case Id.request.secret_keys: {
+ if (resultCode == RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ mSecretKeyId = bundle.getLong(SelectSecretKeyActivity.RESULT_EXTRA_MASTER_KEY_ID);
+ } else {
+ mSecretKeyId = Id.key.none;
+ }
+ updateView();
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpActivity.java
new file mode 100644
index 000000000..5b2750742
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpActivity.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.FragmentTransaction;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.ActionBar.Tab;
+import com.actionbarsherlock.view.MenuItem;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+public class HelpActivity extends SherlockFragmentActivity {
+ public static final String EXTRA_SELECTED_TAB = "selectedTab";
+
+ ViewPager mViewPager;
+ TabsAdapter mTabsAdapter;
+ TextView tabCenter;
+ TextView tabText;
+
+ /**
+ * Menu Items
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mViewPager = new ViewPager(this);
+ mViewPager.setId(R.id.pager);
+
+ setContentView(mViewPager);
+ ActionBar bar = getSupportActionBar();
+ bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ bar.setDisplayShowTitleEnabled(true);
+ bar.setDisplayHomeAsUpEnabled(true);
+
+ mTabsAdapter = new TabsAdapter(this, mViewPager);
+
+ int selectedTab = 0;
+ Intent intent = getIntent();
+ if (intent.getExtras() != null && intent.getExtras().containsKey(EXTRA_SELECTED_TAB)) {
+ selectedTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB);
+ }
+
+ Bundle startBundle = new Bundle();
+ startBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_start);
+ mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_start)),
+ HelpFragmentHtml.class, startBundle, (selectedTab == 0 ? true : false));
+
+ Bundle nfcBundle = new Bundle();
+ nfcBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_nfc_beam);
+ mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_nfc_beam)),
+ HelpFragmentHtml.class, nfcBundle, (selectedTab == 1 ? true : false));
+
+ Bundle changelogBundle = new Bundle();
+ changelogBundle.putInt(HelpFragmentHtml.ARG_HTML_FILE, R.raw.help_changelog);
+ mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_changelog)),
+ HelpFragmentHtml.class, changelogBundle, (selectedTab == 2 ? true : false));
+
+ mTabsAdapter.addTab(bar.newTab().setText(getString(R.string.help_tab_about)),
+ HelpFragmentAbout.class, null, (selectedTab == 3 ? true : false));
+ }
+
+ public static class TabsAdapter extends FragmentPagerAdapter implements ActionBar.TabListener,
+ ViewPager.OnPageChangeListener {
+ private final Context mContext;
+ private final ActionBar mActionBar;
+ private final ViewPager mViewPager;
+ private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
+
+ static final class TabInfo {
+ private final Class<?> clss;
+ private final Bundle args;
+
+ TabInfo(Class<?> _class, Bundle _args) {
+ clss = _class;
+ args = _args;
+ }
+ }
+
+ public TabsAdapter(SherlockFragmentActivity activity, ViewPager pager) {
+ super(activity.getSupportFragmentManager());
+ mContext = activity;
+ mActionBar = activity.getSupportActionBar();
+ mViewPager = pager;
+ mViewPager.setAdapter(this);
+ mViewPager.setOnPageChangeListener(this);
+ }
+
+ public void addTab(ActionBar.Tab tab, Class<?> clss, Bundle args, boolean selected) {
+ TabInfo info = new TabInfo(clss, args);
+ tab.setTag(info);
+ tab.setTabListener(this);
+ mTabs.add(info);
+ mActionBar.addTab(tab, selected);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mTabs.size();
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ TabInfo info = mTabs.get(position);
+ return Fragment.instantiate(mContext, info.clss.getName(), info.args);
+ }
+
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ }
+
+ public void onPageSelected(int position) {
+ mActionBar.setSelectedNavigationItem(position);
+ }
+
+ public void onPageScrollStateChanged(int state) {
+ }
+
+ public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ Object tag = tab.getTag();
+ for (int i = 0; i < mTabs.size(); i++) {
+ if (mTabs.get(i) == tag) {
+ mViewPager.setCurrentItem(i);
+ }
+ }
+ }
+
+ public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+ }
+
+ public void onTabReselected(Tab tab, FragmentTransaction ft) {
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java
new file mode 100644
index 000000000..05891b053
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.nightwhistler.htmlspanner.HtmlSpanner;
+import net.nightwhistler.htmlspanner.JellyBeanSpanFixTextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+
+public class HelpFragmentAbout extends SherlockFragment {
+
+ /**
+ * Workaround for Android Bug. See
+ * http://stackoverflow.com/questions/8748064/starting-activity-from
+ * -fragment-causes-nullpointerexception
+ */
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ setUserVisibleHint(true);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.help_fragment_about, container, false);
+
+ // load html from html file from /res/raw
+ InputStream inputStreamText = getResources().openRawResource(R.raw.help_about);
+
+ TextView versionText = (TextView) view.findViewById(R.id.help_about_version);
+ versionText.setText(getString(R.string.help_about_version) + " " + getVersion());
+
+ JellyBeanSpanFixTextView aboutTextView = (JellyBeanSpanFixTextView) view
+ .findViewById(R.id.help_about_text);
+
+ // load html into textview
+ HtmlSpanner htmlSpanner = new HtmlSpanner();
+ htmlSpanner.setStripExtraWhiteSpace(true);
+ try {
+ aboutTextView.setText(htmlSpanner.fromHtml(inputStreamText));
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while reading raw resources as stream", e);
+ }
+
+ // make links work
+ aboutTextView.setMovementMethod(LinkMovementMethod.getInstance());
+
+ // no flickering when clicking textview for Android < 4
+ aboutTextView.setTextColor(getResources().getColor(android.R.color.black));
+
+ return view;
+ }
+
+ /**
+ * Get the current package version.
+ *
+ * @return The current version.
+ */
+ private String getVersion() {
+ String result = "";
+ try {
+ PackageManager manager = getActivity().getPackageManager();
+ PackageInfo info = manager.getPackageInfo(getActivity().getPackageName(), 0);
+
+ result = String.format("%s (%s)", info.versionName, info.versionCode);
+ } catch (NameNotFoundException e) {
+ Log.w(Constants.TAG, "Unable to get application version: " + e.getMessage());
+ result = "Unable to get application version.";
+ }
+
+ return result;
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentHtml.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentHtml.java
new file mode 100644
index 000000000..9c0bb6ded
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentHtml.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.nightwhistler.htmlspanner.HtmlSpanner;
+import net.nightwhistler.htmlspanner.JellyBeanSpanFixTextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ScrollView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+
+public class HelpFragmentHtml extends SherlockFragment {
+ private Activity mActivity;
+
+ private int htmlFile;
+
+ public static final String ARG_HTML_FILE = "htmlFile";
+
+ /**
+ * Create a new instance of HelpFragmentHtml, providing "htmlFile" as an argument.
+ */
+ static HelpFragmentHtml newInstance(int htmlFile) {
+ HelpFragmentHtml f = new HelpFragmentHtml();
+
+ // Supply html raw file input as an argument.
+ Bundle args = new Bundle();
+ args.putInt(ARG_HTML_FILE, htmlFile);
+ f.setArguments(args);
+
+ return f;
+ }
+
+ /**
+ * Workaround for Android Bug. See
+ * http://stackoverflow.com/questions/8748064/starting-activity-from
+ * -fragment-causes-nullpointerexception
+ */
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ setUserVisibleHint(true);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ htmlFile = getArguments().getInt(ARG_HTML_FILE);
+
+ // load html from html file from /res/raw
+ InputStream inputStreamText = getResources().openRawResource(htmlFile);
+
+ mActivity = getActivity();
+
+ ScrollView scroller = new ScrollView(mActivity);
+ JellyBeanSpanFixTextView text = new JellyBeanSpanFixTextView(mActivity);
+
+ // padding
+ int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, mActivity
+ .getResources().getDisplayMetrics());
+ text.setPadding(padding, padding, padding, 0);
+
+ scroller.addView(text);
+
+ // load html into textview
+ HtmlSpanner htmlSpanner = new HtmlSpanner();
+ htmlSpanner.setStripExtraWhiteSpace(true);
+ try {
+ text.setText(htmlSpanner.fromHtml(inputStreamText));
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while reading raw resources as stream", e);
+ }
+
+ // make links work
+ text.setMovementMethod(LinkMovementMethod.getInstance());
+
+ // no flickering when clicking textview for Android < 4
+ text.setTextColor(getResources().getColor(android.R.color.black));
+
+ return scroller;
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
new file mode 100644
index 000000000..cd5a12df0
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2011 Senecaso
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.ui.dialog.DeleteFileDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+
+
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.view.View;
+import android.widget.Toast;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.google.zxing.integration.android.IntentIntegrator;
+import com.google.zxing.integration.android.IntentResult;
+
+public class ImportKeysActivity extends SherlockFragmentActivity {
+ public static final String ACTION_IMPORT = Constants.INTENT_PREFIX + "IMPORT";
+ public static final String ACTION_IMPORT_FROM_FILE = Constants.INTENT_PREFIX
+ + "IMPORT_FROM_FILE";
+ public static final String ACTION_IMPORT_FROM_QR_CODE = Constants.INTENT_PREFIX
+ + "IMPORT_FROM_QR_CODE";
+ public static final String ACTION_IMPORT_FROM_NFC = Constants.INTENT_PREFIX + "IMPORT_FROM_NFC";
+
+ // only used by IMPORT
+ public static final String EXTRA_TEXT = "text";
+ public static final String EXTRA_KEYRING_BYTES = "keyringBytes";
+
+ // public static final String EXTRA_KEY_ID = "keyId";
+
+ protected String mImportFilename;
+ protected byte[] mImportData;
+
+ protected boolean mDeleteAfterImport = false;
+
+ FileDialogFragment mFileDialog;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.import_keys);
+
+ // set actionbar without home button if called from another app
+ OtherHelper.setActionBarBackButton(this);
+
+ handleActions(getIntent());
+ }
+
+ /**
+ * ActionBar menu is created based on class variables to change it at runtime
+ *
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(1, Id.menu.option.import_from_file, 0, R.string.menu_importFromFile)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(1, Id.menu.option.import_from_qr_code, 1, R.string.menu_importFromQrCode)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(1, Id.menu.option.import_from_nfc, 2, R.string.menu_importFromNfc)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.import_from_file:
+ showImportFromFileDialog();
+ return true;
+
+ case Id.menu.option.import_from_qr_code:
+ importFromQrCode();
+ return true;
+
+ case Id.menu.option.import_from_nfc:
+ importFromNfc();
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+
+ }
+ }
+
+ protected void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ /**
+ * Android Standard Actions
+ */
+ if (Intent.ACTION_VIEW.equals(action)) {
+ // Android's Action when opening file associated to APG (see AndroidManifest.xml)
+ // override action to delegate it to APGs ACTION_IMPORT
+ action = ACTION_IMPORT;
+ }
+
+ /**
+ * APG's own Actions
+ */
+ if (ACTION_IMPORT.equals(action)) {
+ if ("file".equals(intent.getScheme()) && intent.getDataString() != null) {
+ mImportFilename = intent.getData().getPath();
+ mImportData = null;
+ } else if (extras.containsKey(EXTRA_TEXT)) {
+ mImportData = intent.getStringExtra(EXTRA_TEXT).getBytes();
+ mImportFilename = null;
+ } else if (extras.containsKey(EXTRA_KEYRING_BYTES)) {
+ mImportData = intent.getByteArrayExtra(EXTRA_KEYRING_BYTES);
+ mImportFilename = null;
+ }
+ loadKeyListFragment();
+ } else if (ACTION_IMPORT_FROM_FILE.equals(action)) {
+ if ("file".equals(intent.getScheme()) && intent.getDataString() != null) {
+ mImportFilename = intent.getData().getPath();
+ mImportData = null;
+ }
+ showImportFromFileDialog();
+ } else if (ACTION_IMPORT_FROM_QR_CODE.equals(action)) {
+ importFromQrCode();
+ } else if (ACTION_IMPORT_FROM_NFC.equals(action)) {
+ importFromNfc();
+ }
+ }
+
+ public void loadKeyListFragment() {
+ if (mImportData != null || mImportFilename != null) {
+ // generate list of keyrings
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+
+ ImportKeysListFragment listFragment = new ImportKeysListFragment();
+ Bundle args = new Bundle();
+ args.putByteArray(ImportKeysListFragment.ARG_KEYRING_BYTES, mImportData);
+ args.putString(ImportKeysListFragment.ARG_IMPORT_FILENAME, mImportFilename);
+ listFragment.setArguments(args);
+ // replace container in view with fragment
+ fragmentTransaction.replace(R.id.import_keys_list_container, listFragment);
+ fragmentTransaction.commit();
+ }
+ }
+
+ private void importFromQrCode() {
+ new IntentIntegrator(this).initiateScan();
+ }
+
+ private void importFromNfc() {
+ // show nfc help
+ Intent intent = new Intent(this, HelpActivity.class);
+ intent.putExtra(HelpActivity.EXTRA_SELECTED_TAB, 1);
+ startActivityForResult(intent, 0);
+ }
+
+ /**
+ * Show to dialog from where to import keys
+ */
+ public void showImportFromFileDialog() {
+ // Message is received after file is selected
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == FileDialogFragment.MESSAGE_OKAY) {
+ Bundle data = message.getData();
+ mImportFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
+ mDeleteAfterImport = data.getBoolean(FileDialogFragment.MESSAGE_DATA_CHECKED);
+
+ Log.d(Constants.TAG, "mImportFilename: " + mImportFilename);
+ Log.d(Constants.TAG, "mDeleteAfterImport: " + mDeleteAfterImport);
+
+ loadKeyListFragment();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ final Messenger messenger = new Messenger(returnHandler);
+
+ DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() {
+ public void run() {
+ mFileDialog = FileDialogFragment.newInstance(messenger,
+ getString(R.string.title_importKeys),
+ getString(R.string.specifyFileToImportFrom), Constants.path.APP_DIR + "/",
+ null, Id.request.filename);
+
+ mFileDialog.show(getSupportFragmentManager(), "fileDialog");
+ }
+ });
+ }
+
+ // private void importAndSignOld(final long keyId, final String expectedFingerprint) {
+ // if (expectedFingerprint != null && expectedFingerprint.length() > 0) {
+ //
+ // Thread t = new Thread() {
+ // @Override
+ // public void run() {
+ // try {
+ // // TODO: display some sort of spinner here while the user waits
+ //
+ // // TODO: there should be only 1
+ // HkpKeyServer server = new HkpKeyServer(mPreferences.getKeyServers()[0]);
+ // String encodedKey = server.get(keyId);
+ //
+ // PGPKeyRing keyring = PGPHelper.decodeKeyRing(new ByteArrayInputStream(
+ // encodedKey.getBytes()));
+ // if (keyring != null && keyring instanceof PGPPublicKeyRing) {
+ // PGPPublicKeyRing publicKeyRing = (PGPPublicKeyRing) keyring;
+ //
+ // // make sure the fingerprints match before we cache this thing
+ // String actualFingerprint = PGPHelper.convertFingerprintToHex(publicKeyRing
+ // .getPublicKey().getFingerprint());
+ // if (expectedFingerprint.equals(actualFingerprint)) {
+ // // store the signed key in our local cache
+ // int retval = PGPMain.storeKeyRingInCache(publicKeyRing);
+ // if (retval != Id.return_value.ok
+ // && retval != Id.return_value.updated) {
+ // status.putString(EXTRA_ERROR,
+ // "Failed to store signed key in local cache");
+ // } else {
+ // Intent intent = new Intent(ImportFromQRCodeActivity.this,
+ // SignKeyActivity.class);
+ // intent.putExtra(EXTRA_KEY_ID, keyId);
+ // startActivityForResult(intent, Id.request.sign_key);
+ // }
+ // } else {
+ // status.putString(
+ // EXTRA_ERROR,
+ // "Scanned fingerprint does NOT match the fingerprint of the received key. You shouldnt trust this key.");
+ // }
+ // }
+ // } catch (QueryException e) {
+ // Log.e(TAG, "Failed to query KeyServer", e);
+ // status.putString(EXTRA_ERROR, "Failed to query KeyServer");
+ // status.putInt(Constants.extras.STATUS, Id.message.done);
+ // } catch (IOException e) {
+ // Log.e(TAG, "Failed to query KeyServer", e);
+ // status.putString(EXTRA_ERROR, "Failed to query KeyServer");
+ // status.putInt(Constants.extras.STATUS, Id.message.done);
+ // }
+ // }
+ // };
+ //
+ // t.setName("KeyExchange Download Thread");
+ // t.setDaemon(true);
+ // t.start();
+ // }
+ // }
+
+ public void scanAgainOnClick(View view) {
+ new IntentIntegrator(this).initiateScan();
+ }
+
+ public void finishOnClick(View view) {
+ finish();
+ }
+
+ public void importOnClick(View view) {
+ Log.d(Constants.TAG, "Import key button clicked!");
+
+ importKeys();
+ }
+
+ /**
+ * Import keys with mImportData
+ */
+ public void importKeys() {
+ if (mImportData != null || mImportFilename != null) {
+ Log.d(Constants.TAG, "importKeys started");
+
+ // Send all information needed to service to import key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_IMPORT_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ // TODO: check for key type?
+ // data.putInt(ApgIntentService.IMPORT_KEY_TYPE, Id.type.secret_key);
+
+ if (mImportData != null) {
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_BYTES);
+ data.putByteArray(KeychainIntentService.IMPORT_BYTES, mImportData);
+ } else {
+ data.putInt(KeychainIntentService.TARGET, KeychainIntentService.TARGET_FILE);
+ data.putString(KeychainIntentService.IMPORT_FILENAME, mImportFilename);
+ }
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after importing is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_importing, ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle returnData = message.getData();
+
+ int added = returnData.getInt(KeychainIntentService.RESULT_IMPORT_ADDED);
+ int updated = returnData.getInt(KeychainIntentService.RESULT_IMPORT_UPDATED);
+ int bad = returnData.getInt(KeychainIntentService.RESULT_IMPORT_BAD);
+ String toastMessage;
+ if (added > 0 && updated > 0) {
+ toastMessage = getString(R.string.keysAddedAndUpdated, added, updated);
+ } else if (added > 0) {
+ toastMessage = getString(R.string.keysAdded, added);
+ } else if (updated > 0) {
+ toastMessage = getString(R.string.keysUpdated, updated);
+ } else {
+ toastMessage = getString(R.string.noKeysAddedOrUpdated);
+ }
+ Toast.makeText(ImportKeysActivity.this, toastMessage, Toast.LENGTH_SHORT)
+ .show();
+ if (bad > 0) {
+ AlertDialog.Builder alert = new AlertDialog.Builder(
+ ImportKeysActivity.this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(ImportKeysActivity.this.getString(
+ R.string.badKeysEncountered, bad));
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ alert.setCancelable(true);
+ alert.create().show();
+ } else if (mDeleteAfterImport) {
+ // everything went well, so now delete, if that was turned on
+ DeleteFileDialogFragment deleteFileDialog = DeleteFileDialogFragment
+ .newInstance(mImportFilename);
+ deleteFileDialog.show(getSupportFragmentManager(), "deleteDialog");
+ }
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ } else {
+ Toast.makeText(this, R.string.error_nothingImport, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ public void signAndUploadOnClick(View view) {
+ // first, import!
+ importOnClick(view);
+
+ // TODO: implement sign and upload!
+ Toast.makeText(ImportKeysActivity.this, "Not implemented right now!", Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ try {
+ String path = data.getData().getPath();
+ Log.d(Constants.TAG, "path=" + path);
+
+ // set filename used in export/import dialogs
+ mFileDialog.setFilename(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!", e);
+ }
+ }
+ return;
+ }
+ case IntentIntegrator.REQUEST_CODE: {
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode,
+ data);
+ if (scanResult != null && scanResult.getFormatName() != null) {
+
+ // mScannedContent = scanResult.getContents();
+
+ mImportData = scanResult.getContents().getBytes();
+ mImportFilename = null;
+
+ // mContentView.setText(mScannedContent);
+ // String[] bits = scanResult.getContents().split(",");
+ // if (bits.length != 2) {
+ // return; // dont know how to handle this. Not a valid code
+ // }
+ //
+ // long keyId = Long.parseLong(bits[0]);
+ // String expectedFingerprint = bits[1];
+
+ // importAndSign(keyId, expectedFingerprint);
+ }
+
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ loadKeyListFragment();
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java
new file mode 100644
index 000000000..170b459be
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.ui.widget.ImportKeysListLoader;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockListFragment;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.content.Loader;
+import android.support.v4.app.LoaderManager;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.SimpleAdapter;
+
+public class ImportKeysListFragment extends SherlockListFragment implements
+ LoaderManager.LoaderCallbacks<List<Map<String, String>>> {
+ public static String ARG_KEYRING_BYTES = "bytes";
+ public static String ARG_IMPORT_FILENAME = "filename";
+
+ byte[] mKeyringBytes;
+ String mImportFilename;
+
+ private Activity mActivity;
+ private SimpleAdapter mAdapter;
+
+ @Override
+ public void onListItemClick(ListView listView, View view, int position, long id) {
+ // Map<String, String> item = (Map<String, String>) listView.getItemAtPosition(position);
+ // String userId = item.get(ImportKeysListLoader.MAP_ATTR_USER_ID);
+ }
+
+ /**
+ * Resume is called after rotating
+ */
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // reload list
+ getLoaderManager().restartLoader(0, null, this);
+ }
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mActivity = this.getActivity();
+
+ mKeyringBytes = getArguments().getByteArray(ARG_KEYRING_BYTES);
+ mImportFilename = getArguments().getString(ARG_IMPORT_FILENAME);
+
+ // register long press context menu
+ registerForContextMenu(getListView());
+
+ // Give some text to display if there is no data. In a real
+ // application this would come from a resource.
+ setEmptyText(mActivity.getString(R.string.error_nothingImport));
+
+ // Create an empty adapter we will use to display the loaded data.
+ String[] from = new String[] {};
+ int[] to = new int[] {};
+ List<Map<String, String>> data = new ArrayList<Map<String, String>>();
+ mAdapter = new SimpleAdapter(getActivity(), data, android.R.layout.two_line_list_item,
+ from, to);
+ setListAdapter(mAdapter);
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ @Override
+ public Loader<List<Map<String, String>>> onCreateLoader(int id, Bundle args) {
+ return new ImportKeysListLoader(mActivity, mKeyringBytes, mImportFilename);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<List<Map<String, String>>> loader,
+ List<Map<String, String>> data) {
+ // Set the new data in the adapter.
+ // for (String item : data) {
+ // mAdapter.add(item);
+ // }
+ Log.d(Constants.TAG, "data: " + data);
+ // TODO: real swapping the data to the already defined adapter doesn't work
+ // Workaround: recreate adapter!
+ // http://stackoverflow.com/questions/2356091/android-add-function-of-arrayadapter-not-working
+ // mAdapter = new ArrayAdapter<String>(mActivity, android.R.layout.two_line_list_item,
+ // data);
+
+ String[] from = new String[] { ImportKeysListLoader.MAP_ATTR_USER_ID,
+ ImportKeysListLoader.MAP_ATTR_FINGERPINT };
+ int[] to = new int[] { android.R.id.text1, android.R.id.text2 };
+ mAdapter = new SimpleAdapter(getActivity(), data, android.R.layout.two_line_list_item,
+ from, to);
+
+ mAdapter.notifyDataSetChanged();
+ setListAdapter(mAdapter);
+
+ // The list should now be shown.
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<Map<String, String>>> loader) {
+ // Clear the data in the adapter.
+ // Not available in SimpleAdapter!
+ // mAdapter.clear();
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java
new file mode 100644
index 000000000..a128263c1
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment;
+import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.ProgressDialog;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+import android.widget.Toast;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+public class KeyListActivity extends SherlockFragmentActivity {
+
+ protected String mExportFilename = Constants.path.APP_DIR + "/";
+
+ protected String mImportData;
+ protected boolean mDeleteAfterImport = false;
+
+ protected int mKeyType;
+
+ FileDialogFragment mFileDialog;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+
+ handleActions(getIntent());
+ }
+
+ // TODO: needed?
+ // @Override
+ // protected void onNewIntent(Intent intent) {
+ // super.onNewIntent(intent);
+ // handleActions(intent);
+ // }
+
+ protected void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ /**
+ * Android Standard Actions
+ */
+ String searchString = null;
+ if (Intent.ACTION_SEARCH.equals(action)) {
+ searchString = extras.getString(SearchManager.QUERY);
+ if (searchString != null && searchString.trim().length() == 0) {
+ searchString = null;
+ }
+ }
+
+ // if (searchString == null) {
+ // mFilterLayout.setVisibility(View.GONE);
+ // } else {
+ // mFilterLayout.setVisibility(View.VISIBLE);
+ // mFilterInfo.setText(getString(R.string.filterInfo, searchString));
+ // }
+ //
+ // if (mListAdapter != null) {
+ // mListAdapter.cleanup();
+ // }
+ // mListAdapter = new KeyListAdapter(this, searchString);
+ // mList.setAdapter(mListAdapter);
+
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.filename: {
+ if (resultCode == RESULT_OK && data != null) {
+ try {
+ String path = data.getData().getPath();
+ Log.d(Constants.TAG, "path=" + path);
+
+ // set filename used in export/import dialogs
+ mFileDialog.setFilename(path);
+ } catch (NullPointerException e) {
+ Log.e(Constants.TAG, "Nullpointer while retrieving path!", e);
+ }
+ }
+ return;
+ }
+
+ default: {
+ break;
+ }
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ // TODO: reimplement!
+ // menu.add(3, Id.menu.option.search, 0, R.string.menu_search)
+ // .setIcon(R.drawable.ic_menu_search).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ menu.add(0, Id.menu.option.import_from_file, 5, R.string.menu_importFromFile)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(0, Id.menu.option.export_keys, 6, R.string.menu_exportKeys).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_NEVER | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.import_from_file: {
+ Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class);
+ intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_FILE);
+ startActivityForResult(intentImportFromFile, 0);
+ return true;
+ }
+
+ case Id.menu.option.export_keys: {
+ showExportKeysDialog(-1);
+ return true;
+ }
+
+ // case Id.menu.option.search:
+ // startSearch("", false, null, false);
+ // return true;
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ /**
+ * Show dialog where to export keys
+ *
+ * @param keyRingMasterKeyId
+ * if -1 export all keys
+ */
+ public void showExportKeysDialog(final long keyRingMasterKeyId) {
+ // Message is received after file is selected
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == FileDialogFragment.MESSAGE_OKAY) {
+ Bundle data = message.getData();
+ mExportFilename = data.getString(FileDialogFragment.MESSAGE_DATA_FILENAME);
+
+ exportKeys(keyRingMasterKeyId);
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ final Messenger messenger = new Messenger(returnHandler);
+
+ DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() {
+ public void run() {
+ String title = null;
+ if (keyRingMasterKeyId != -1) {
+ // single key export
+ title = getString(R.string.title_exportKey);
+ } else {
+ title = getString(R.string.title_exportKeys);
+ }
+
+ String message = null;
+ if (mKeyType == Id.type.public_key) {
+ message = getString(R.string.specifyFileToExportTo);
+ } else {
+ message = getString(R.string.specifyFileToExportSecretKeysTo);
+ }
+
+ mFileDialog = FileDialogFragment.newInstance(messenger, title, message,
+ mExportFilename, null, Id.request.filename);
+
+ mFileDialog.show(getSupportFragmentManager(), "fileDialog");
+ }
+ });
+ }
+
+ /**
+ * Show dialog to delete key
+ *
+ * @param keyRingId
+ */
+ public void showDeleteKeyDialog(long keyRingId) {
+ // Message is received after key is deleted
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) {
+ // no further actions needed
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ DeleteKeyDialogFragment deleteKeyDialog = DeleteKeyDialogFragment.newInstance(messenger,
+ keyRingId, mKeyType);
+
+ deleteKeyDialog.show(getSupportFragmentManager(), "deleteKeyDialog");
+ }
+
+ /**
+ * Export keys
+ *
+ * @param keyRingMasterKeyId
+ * if -1 export all keys
+ */
+ public void exportKeys(long keyRingMasterKeyId) {
+ Log.d(Constants.TAG, "exportKeys started");
+
+ // Send all information needed to service to export key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_EXPORT_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ data.putString(KeychainIntentService.EXPORT_FILENAME, mExportFilename);
+ data.putInt(KeychainIntentService.EXPORT_KEY_TYPE, mKeyType);
+
+ if (keyRingMasterKeyId == -1) {
+ data.putBoolean(KeychainIntentService.EXPORT_ALL, true);
+ } else {
+ data.putLong(KeychainIntentService.EXPORT_KEY_RING_MASTER_KEY_ID, keyRingMasterKeyId);
+ }
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after exporting is done in ApgService
+ KeychainIntentServiceHandler exportHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_exporting, ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle returnData = message.getData();
+
+ int exported = returnData.getInt(KeychainIntentService.RESULT_EXPORT);
+ String toastMessage;
+ if (exported == 1) {
+ toastMessage = getString(R.string.keyExported);
+ } else if (exported > 0) {
+ toastMessage = getString(R.string.keysExported, exported);
+ } else {
+ toastMessage = getString(R.string.noKeysExported);
+ }
+ Toast.makeText(KeyListActivity.this, toastMessage, Toast.LENGTH_SHORT).show();
+
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(exportHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ exportHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java
new file mode 100644
index 000000000..9e90e254c
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.ui.widget.ExpandableListFragment;
+import org.sufficientlysecure.keychain.R;
+
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+public class KeyListFragment extends ExpandableListFragment {
+ protected KeyListActivity mKeyListActivity;
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mKeyListActivity = (KeyListActivity) getActivity();
+
+ // register long press context menu
+ registerForContextMenu(getListView());
+
+ // Give some text to display if there is no data. In a real
+ // application this would come from a resource.
+ setEmptyText(getString(R.string.listEmpty));
+ }
+
+ /**
+ * Context Menu on Long Click
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ menu.add(0, Id.menu.export, 5, R.string.menu_exportKey);
+ menu.add(0, Id.menu.delete, 111, R.string.menu_deleteKey);
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem item) {
+ ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo();
+
+ // expInfo.id would also return row id of childs, but we always want to get the row id of
+ // the group item, thus we are using the following way
+ int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition);
+ long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition);
+
+ switch (item.getItemId()) {
+ case Id.menu.export:
+ long masterKeyId = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId);
+ if (masterKeyId == -1) {
+ masterKeyId = ProviderHelper.getSecretMasterKeyId(mKeyListActivity, keyRingRowId);
+ }
+
+ mKeyListActivity.showExportKeysDialog(masterKeyId);
+ return true;
+
+ case Id.menu.delete:
+ mKeyListActivity.showDeleteKeyDialog(keyRingRowId);
+ return true;
+
+ default:
+ return super.onContextItemSelected(item);
+
+ }
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java
new file mode 100644
index 000000000..adc8656d0
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicActivity.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+public class KeyListPublicActivity extends KeyListActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mKeyType = Id.type.public_key;
+
+ setContentView(R.layout.key_list_public_activity);
+
+ mExportFilename = Constants.path.APP_DIR + "/pubexport.asc";
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ menu.add(1, Id.menu.option.key_server, 1, R.string.menu_keyServer)
+ .setIcon(R.drawable.ic_menu_search_list)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS);
+ menu.add(1, Id.menu.option.import_from_qr_code, 2, R.string.menu_importFromQrCode)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(1, Id.menu.option.import_from_nfc, 3, R.string.menu_importFromNfc)
+ .setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.key_server: {
+ startActivityForResult(new Intent(this, KeyServerQueryActivity.class), 0);
+
+ return true;
+ }
+ case Id.menu.option.import_from_file: {
+ Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class);
+ intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_FILE);
+ startActivityForResult(intentImportFromFile, 0);
+
+ return true;
+ }
+
+ case Id.menu.option.import_from_qr_code: {
+ Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class);
+ intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_QR_CODE);
+ startActivityForResult(intentImportFromFile, Id.request.import_from_qr_code);
+
+ return true;
+ }
+
+ case Id.menu.option.import_from_nfc: {
+ Intent intentImportFromFile = new Intent(this, ImportKeysActivity.class);
+ intentImportFromFile.setAction(ImportKeysActivity.ACTION_IMPORT_FROM_NFC);
+ startActivityForResult(intentImportFromFile, 0);
+
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ // @Override
+ // protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // switch (requestCode) {
+ // case Id.request.look_up_key_id: {
+ // if (resultCode == RESULT_CANCELED || data == null
+ // || data.getStringExtra(KeyServerQueryActivity.RESULT_EXTRA_TEXT) == null) {
+ // return;
+ // }
+ //
+ // Intent intent = new Intent(this, KeyListPublicActivity.class);
+ // intent.setAction(KeyListPublicActivity.ACTION_IMPORT);
+ // intent.putExtra(KeyListPublicActivity.EXTRA_TEXT,
+ // data.getStringExtra(KeyListActivity.EXTRA_TEXT));
+ // handleActions(intent);
+ // break;
+ // }
+ //
+ // default: {
+ // super.onActivityResult(requestCode, resultCode, data);
+ // break;
+ // }
+ // }
+ // }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java
new file mode 100644
index 000000000..648b09ef7
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.ui.widget.KeyListAdapter;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.app.LoaderManager;
+import android.view.ContextMenu;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+public class KeyListPublicFragment extends KeyListFragment implements
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ private KeyListPublicActivity mKeyListPublicActivity;
+
+ private KeyListAdapter mAdapter;
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mKeyListPublicActivity = (KeyListPublicActivity) getActivity();
+
+ mAdapter = new KeyListAdapter(mKeyListPublicActivity, null, Id.type.public_key);
+ setListAdapter(mAdapter);
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ // id is -1 as the child cursors are numbered 0,...,n
+ getLoaderManager().initLoader(-1, null, this);
+ }
+
+ /**
+ * Context Menu on Long Click
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ menu.add(0, Id.menu.update, 1, R.string.menu_updateKey);
+ menu.add(0, Id.menu.signKey, 2, R.string.menu_signKey);
+ menu.add(0, Id.menu.exportToServer, 3, R.string.menu_exportKeyToServer);
+ menu.add(0, Id.menu.share, 6, R.string.menu_share);
+ menu.add(0, Id.menu.share_qr_code, 7, R.string.menu_shareQrCode);
+ menu.add(0, Id.menu.share_nfc, 8, R.string.menu_shareNfc);
+
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem item) {
+ ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo();
+
+ // expInfo.id would also return row id of childs, but we always want to get the row id of
+ // the group item, thus we are using the following way
+ int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition);
+ long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition);
+
+ switch (item.getItemId()) {
+ case Id.menu.update:
+ long updateKeyId = 0;
+ PGPPublicKeyRing updateKeyRing = ProviderHelper.getPGPPublicKeyRingByRowId(
+ mKeyListActivity, keyRingRowId);
+ if (updateKeyRing != null) {
+ updateKeyId = PgpHelper.getMasterKey(updateKeyRing).getKeyID();
+ }
+ if (updateKeyId == 0) {
+ // this shouldn't happen
+ return true;
+ }
+
+ Intent queryIntent = new Intent(mKeyListActivity, KeyServerQueryActivity.class);
+ queryIntent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID_AND_RETURN);
+ queryIntent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, updateKeyId);
+
+ // TODO: lookup??
+ startActivityForResult(queryIntent, Id.request.look_up_key_id);
+
+ return true;
+
+ case Id.menu.exportToServer:
+ Intent uploadIntent = new Intent(mKeyListActivity, KeyServerUploadActivity.class);
+ uploadIntent.setAction(KeyServerUploadActivity.ACTION_EXPORT_KEY_TO_SERVER);
+ uploadIntent.putExtra(KeyServerUploadActivity.EXTRA_KEYRING_ROW_ID, keyRingRowId);
+ startActivityForResult(uploadIntent, Id.request.export_to_server);
+
+ return true;
+
+ case Id.menu.signKey:
+ long keyId = 0;
+ PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingByRowId(
+ mKeyListActivity, keyRingRowId);
+ if (signKeyRing != null) {
+ keyId = PgpHelper.getMasterKey(signKeyRing).getKeyID();
+ }
+ if (keyId == 0) {
+ // this shouldn't happen
+ return true;
+ }
+
+ Intent signIntent = new Intent(mKeyListActivity, SignKeyActivity.class);
+ signIntent.putExtra(SignKeyActivity.EXTRA_KEY_ID, keyId);
+ startActivity(signIntent);
+
+ return true;
+
+ case Id.menu.share_qr_code:
+ // get master key id using row id
+ long masterKeyId = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId);
+
+ Intent qrCodeIntent = new Intent(mKeyListActivity, ShareActivity.class);
+ qrCodeIntent.setAction(ShareActivity.ACTION_SHARE_KEYRING_WITH_QR_CODE);
+ qrCodeIntent.putExtra(ShareActivity.EXTRA_MASTER_KEY_ID, masterKeyId);
+ startActivityForResult(qrCodeIntent, 0);
+
+ return true;
+
+ case Id.menu.share_nfc:
+ // get master key id using row id
+ long masterKeyId2 = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId);
+
+ Intent nfcIntent = new Intent(mKeyListActivity, ShareNfcBeamActivity.class);
+ nfcIntent.setAction(ShareNfcBeamActivity.ACTION_SHARE_KEYRING_WITH_NFC);
+ nfcIntent.putExtra(ShareNfcBeamActivity.EXTRA_MASTER_KEY_ID, masterKeyId2);
+ startActivityForResult(nfcIntent, 0);
+
+ return true;
+
+ case Id.menu.share:
+ // get master key id using row id
+ long masterKeyId3 = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId);
+
+ Intent shareIntent = new Intent(mKeyListActivity, ShareActivity.class);
+ shareIntent.setAction(ShareActivity.ACTION_SHARE_KEYRING);
+ shareIntent.putExtra(ShareActivity.EXTRA_MASTER_KEY_ID, masterKeyId3);
+ startActivityForResult(shareIntent, 0);
+
+ return true;
+
+ default:
+ return super.onContextItemSelected(item);
+
+ }
+ }
+
+ // 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 + " ASC";
+
+ @Override
+ public Loader<Cursor> 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<Cursor> loader, Cursor data) {
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.setGroupCursor(data);
+
+ // The list should now be shown.
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> 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.setGroupCursor(null);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java
new file mode 100644
index 000000000..63d06edaf
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+
+public class KeyListSecretActivity extends KeyListActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mKeyType = Id.type.secret_key;
+
+ setContentView(R.layout.key_list_secret_activity);
+
+ mExportFilename = Constants.path.APP_DIR + "/secexport.asc";
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ menu.add(1, Id.menu.option.create, 1, R.string.menu_createKey).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case Id.menu.option.create: {
+ createKey();
+ return true;
+ }
+
+ default: {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+ }
+
+ public void checkPassPhraseAndEdit(long keyId) {
+ String passPhrase = PassphraseCacheService.getCachedPassphrase(this, keyId);
+ if (passPhrase == null) {
+ showPassphraseDialog(keyId);
+ } else {
+ PgpMain.setEditPassPhrase(passPhrase);
+ editKey(keyId);
+ }
+ }
+
+ private void showPassphraseDialog(final long secretKeyId) {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ String passPhrase = PassphraseCacheService.getCachedPassphrase(
+ KeyListSecretActivity.this, secretKeyId);
+ PgpMain.setEditPassPhrase(passPhrase);
+ editKey(secretKeyId);
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ try {
+ PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(
+ KeyListSecretActivity.this, messenger, secretKeyId);
+
+ passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
+ } catch (PgpMain.PgpGeneralException e) {
+ Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
+ // send message to handler to start encryption directly
+ returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
+ }
+ }
+
+ private void createKey() {
+ PgpMain.setEditPassPhrase("");
+ Intent intent = new Intent(EditKeyActivity.ACTION_CREATE_KEY);
+ startActivityForResult(intent, 0);
+ }
+
+ private void editKey(long keyId) {
+ Intent intent = new Intent(EditKeyActivity.ACTION_EDIT_KEY);
+ intent.putExtra(EditKeyActivity.EXTRA_KEY_ID, keyId);
+ startActivityForResult(intent, 0);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java
new file mode 100644
index 000000000..f4ba2b4d9
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.util.ArrayList;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.ui.widget.KeyListAdapter;
+import org.sufficientlysecure.keychain.R;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.app.LoaderManager;
+import android.view.ContextMenu;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.ExpandableListView;
+import android.widget.Toast;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+public class KeyListSecretFragment extends KeyListFragment implements
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ private KeyListSecretActivity mKeyListSecretActivity;
+
+ private KeyListAdapter mAdapter;
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mKeyListSecretActivity = (KeyListSecretActivity) getActivity();
+
+ mAdapter = new KeyListAdapter(mKeyListSecretActivity, null, Id.type.secret_key);
+ setListAdapter(mAdapter);
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ // id is -1 as the child cursors are numbered 0,...,n
+ getLoaderManager().initLoader(-1, null, this);
+ }
+
+ /**
+ * Context Menu on Long Click
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ menu.add(0, Id.menu.edit, 0, R.string.menu_editKey);
+ }
+
+ @Override
+ public boolean onContextItemSelected(android.view.MenuItem item) {
+ ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo();
+
+ // expInfo.id would also return row id of childs, but we always want to get the row id of
+ // the group item, thus we are using the following way
+ int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition);
+ long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition);
+
+ // get master key id using row id
+ long masterKeyId = ProviderHelper
+ .getSecretMasterKeyId(mKeyListSecretActivity, keyRingRowId);
+
+ switch (item.getItemId()) {
+ case Id.menu.edit:
+ mKeyListSecretActivity.checkPassPhraseAndEdit(masterKeyId);
+
+ return true;
+
+ default:
+ return super.onContextItemSelected(item);
+
+ }
+ }
+
+
+ // 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 + " ASC";
+
+ @Override
+ public Loader<Cursor> 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.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);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.setGroupCursor(data);
+
+ // The list should now be shown.
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> 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.setGroupCursor(null);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java
new file mode 100644
index 000000000..df8b7b374
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.KeyServer.KeyInfo;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class KeyServerQueryActivity extends SherlockFragmentActivity {
+
+ // possible intent actions for this activity
+ public static final String ACTION_LOOK_UP_KEY_ID = Constants.INTENT_PREFIX + "LOOK_UP_KEY_ID";
+ public static final String ACTION_LOOK_UP_KEY_ID_AND_RETURN = Constants.INTENT_PREFIX
+ + "LOOK_UP_KEY_ID_AND_RETURN";
+
+ public static final String EXTRA_KEY_ID = "keyId";
+
+ public static final String RESULT_EXTRA_TEXT = "text";
+
+ private ListView mList;
+ private EditText mQuery;
+ private Button mSearch;
+ private KeyInfoListAdapter mAdapter;
+ private Spinner mKeyServer;
+
+ private int mQueryType;
+ private String mQueryString;
+ private long mQueryId;
+ private volatile List<KeyInfo> mSearchResult;
+ private volatile String mKeyData;
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, KeyListPublicActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.key_server_query_layout);
+
+ mQuery = (EditText) findViewById(R.id.query);
+ mSearch = (Button) findViewById(R.id.btn_search);
+ mList = (ListView) findViewById(R.id.list);
+ mAdapter = new KeyInfoListAdapter(this);
+ mList.setAdapter(mAdapter);
+
+ mKeyServer = (Spinner) findViewById(R.id.keyServer);
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+ android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
+ .getKeyServers());
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mKeyServer.setAdapter(adapter);
+ if (adapter.getCount() > 0) {
+ mKeyServer.setSelection(0);
+ } else {
+ mSearch.setEnabled(false);
+ }
+
+ mList.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> adapter, View view, int position, long keyId) {
+ get(keyId);
+ }
+ });
+
+ mSearch.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ String query = mQuery.getText().toString();
+ search(query);
+ }
+ });
+
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ if (ACTION_LOOK_UP_KEY_ID.equals(action) || ACTION_LOOK_UP_KEY_ID_AND_RETURN.equals(action)) {
+ long keyId = intent.getLongExtra(EXTRA_KEY_ID, 0);
+ if (keyId != 0) {
+ String query = "0x" + PgpHelper.keyToHex(keyId);
+ mQuery.setText(query);
+ search(query);
+ }
+ }
+ }
+
+ private void search(String query) {
+ mQueryType = Id.keyserver.search;
+ mQueryString = query;
+ mAdapter.setKeys(new ArrayList<KeyInfo>());
+
+ start();
+ }
+
+ private void get(long keyId) {
+ mQueryType = Id.keyserver.get;
+ mQueryId = keyId;
+
+ start();
+ }
+
+ private void start() {
+ Log.d(Constants.TAG, "start search with service");
+
+ // Send all information needed to service to query keys in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_QUERY_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ String server = (String) mKeyServer.getSelectedItem();
+ data.putString(KeychainIntentService.QUERY_KEY_SERVER, server);
+
+ data.putInt(KeychainIntentService.QUERY_KEY_TYPE, mQueryType);
+
+ if (mQueryType == Id.keyserver.search) {
+ data.putString(KeychainIntentService.QUERY_KEY_STRING, mQueryString);
+ } else if (mQueryType == Id.keyserver.get) {
+ data.putLong(KeychainIntentService.QUERY_KEY_ID, mQueryId);
+ }
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after querying is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this,
+ R.string.progress_querying, ProgressDialog.STYLE_SPINNER) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get returned data bundle
+ Bundle returnData = message.getData();
+
+ if (mQueryType == Id.keyserver.search) {
+ mSearchResult = returnData
+ .getParcelableArrayList(KeychainIntentService.RESULT_QUERY_KEY_SEARCH_RESULT);
+ } else if (mQueryType == Id.keyserver.get) {
+ mKeyData = returnData.getString(KeychainIntentService.RESULT_QUERY_KEY_KEY_DATA);
+ }
+
+ // TODO: IMPROVE CODE!!! some global variables can be avoided!!!
+ if (mQueryType == Id.keyserver.search) {
+ if (mSearchResult != null) {
+ Toast.makeText(KeyServerQueryActivity.this,
+ getString(R.string.keysFound, mSearchResult.size()),
+ Toast.LENGTH_SHORT).show();
+ mAdapter.setKeys(mSearchResult);
+ }
+ } else if (mQueryType == Id.keyserver.get) {
+ Intent orgIntent = getIntent();
+ if (ACTION_LOOK_UP_KEY_ID_AND_RETURN.equals(orgIntent.getAction())) {
+ if (mKeyData != null) {
+ Intent intent = new Intent();
+ intent.putExtra(RESULT_EXTRA_TEXT, mKeyData);
+ setResult(RESULT_OK, intent);
+ } else {
+ setResult(RESULT_CANCELED);
+ }
+ finish();
+ } else {
+ if (mKeyData != null) {
+ Intent intent = new Intent(KeyServerQueryActivity.this,
+ ImportKeysActivity.class);
+ intent.setAction(ImportKeysActivity.ACTION_IMPORT);
+ intent.putExtra(ImportKeysActivity.EXTRA_TEXT, mKeyData);
+ startActivity(intent);
+ }
+ }
+ }
+
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+
+ public class KeyInfoListAdapter extends BaseAdapter {
+ protected LayoutInflater mInflater;
+ protected Activity mActivity;
+ protected List<KeyInfo> mKeys;
+
+ public KeyInfoListAdapter(Activity activity) {
+ mActivity = activity;
+ mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mKeys = new ArrayList<KeyInfo>();
+ }
+
+ public void setKeys(List<KeyInfo> keys) {
+ mKeys = keys;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public int getCount() {
+ return mKeys.size();
+ }
+
+ public Object getItem(int position) {
+ return mKeys.get(position);
+ }
+
+ public long getItemId(int position) {
+ return mKeys.get(position).keyId;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ KeyInfo keyInfo = mKeys.get(position);
+
+ View view = mInflater.inflate(R.layout.key_server_query_result_item, null);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mainUserId.setText(R.string.unknownUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ keyId.setText(R.string.noKey);
+ TextView algorithm = (TextView) view.findViewById(R.id.algorithm);
+ algorithm.setText("");
+ TextView status = (TextView) view.findViewById(R.id.status);
+ status.setText("");
+
+ String userId = keyInfo.userIds.get(0);
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ keyId.setText(PgpHelper.getSmallFingerPrint(keyInfo.keyId));
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ algorithm.setText("" + keyInfo.size + "/" + keyInfo.algorithm);
+
+ if (keyInfo.revoked != null) {
+ status.setText("revoked");
+ } else {
+ status.setVisibility(View.GONE);
+ }
+
+ LinearLayout ll = (LinearLayout) view.findViewById(R.id.list);
+ if (keyInfo.userIds.size() == 1) {
+ ll.setVisibility(View.GONE);
+ } else {
+ boolean first = true;
+ boolean second = true;
+ for (String uid : keyInfo.userIds) {
+ if (first) {
+ first = false;
+ continue;
+ }
+ if (!second) {
+ View sep = new View(mActivity);
+ sep.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 1));
+ sep.setBackgroundResource(android.R.drawable.divider_horizontal_dark);
+ ll.addView(sep);
+ }
+ TextView uidView = (TextView) mInflater.inflate(
+ R.layout.key_server_query_result_user_id, null);
+ uidView.setText(uid);
+ ll.addView(uidView);
+ second = false;
+ }
+ }
+
+ return view;
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java
new file mode 100644
index 000000000..657044f2b
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2011 Senecaso
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.Spinner;
+import android.widget.Toast;
+
+/**
+ * gpg --send-key activity
+ *
+ * Sends the selected public key to a key server
+ */
+public class KeyServerUploadActivity extends SherlockFragmentActivity {
+
+ // Not used in sourcode, but listed in AndroidManifest!
+ public static final String ACTION_EXPORT_KEY_TO_SERVER = Constants.INTENT_PREFIX
+ + "EXPORT_KEY_TO_SERVER";
+
+ public static final String EXTRA_KEYRING_ROW_ID = "keyId";
+
+ private Button export;
+ private Spinner keyServer;
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, KeyListPublicActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.key_server_export_layout);
+
+ export = (Button) findViewById(R.id.btn_export_to_server);
+ keyServer = (Spinner) findViewById(R.id.keyServer);
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+ android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
+ .getKeyServers());
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ keyServer.setAdapter(adapter);
+ if (adapter.getCount() > 0) {
+ keyServer.setSelection(0);
+ } else {
+ export.setEnabled(false);
+ }
+
+ export.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ uploadKey();
+ }
+ });
+ }
+
+ private void uploadKey() {
+ // Send all information needed to service to upload key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_UPLOAD_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ int keyRingId = getIntent().getIntExtra(EXTRA_KEYRING_ROW_ID, -1);
+ data.putInt(KeychainIntentService.UPLOAD_KEY_KEYRING_ROW_ID, keyRingId);
+
+ String server = (String) keyServer.getSelectedItem();
+ data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after uploading is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, R.string.progress_exporting,
+ ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+
+ Toast.makeText(KeyServerUploadActivity.this, R.string.keySendSuccess,
+ Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/MainActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/MainActivity.java
new file mode 100644
index 000000000..447801e55
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/MainActivity.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+public class MainActivity extends SherlockActivity {
+
+ public void manageKeysOnClick(View view) {
+ // used instead of startActivity set actionbar based on callingPackage
+ startActivityForResult(new Intent(this, KeyListPublicActivity.class), 0);
+ }
+
+ public void myKeysOnClick(View view) {
+ // used instead of startActivity set actionbar based on callingPackage
+ startActivityForResult(new Intent(this, KeyListSecretActivity.class), 0);
+ }
+
+ public void encryptOnClick(View view) {
+ Intent intent = new Intent(MainActivity.this, EncryptActivity.class);
+ intent.setAction(EncryptActivity.ACTION_ENCRYPT);
+ // used instead of startActivity set actionbar based on callingPackage
+ startActivityForResult(intent, 0);
+ }
+
+ public void decryptOnClick(View view) {
+ Intent intent = new Intent(MainActivity.this, DecryptActivity.class);
+ intent.setAction(DecryptActivity.ACTION_DECRYPT);
+ // used instead of startActivity set actionbar based on callingPackage
+ startActivityForResult(intent, 0);
+ }
+
+ public void scanQrcodeOnClick(View view) {
+ Intent intent = new Intent(this, ImportKeysActivity.class);
+ startActivityForResult(intent, 0);
+ }
+
+ public void helpOnClick(View view) {
+ startActivity(new Intent(this, HelpActivity.class));
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, Id.menu.option.preferences, 0, R.string.menu_preferences)
+ .setIcon(R.drawable.ic_menu_settings)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case Id.menu.option.preferences:
+ startActivity(new Intent(this, PreferencesActivity.class));
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesActivity.java
new file mode 100644
index 000000000..3c070c6db
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesActivity.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.spongycastle.bcpg.HashAlgorithmTags;
+import org.spongycastle.openpgp.PGPEncryptedData;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.ui.widget.IntegerListPreference;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockPreferenceActivity;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+
+public class PreferencesActivity extends SherlockPreferenceActivity {
+ private IntegerListPreference mPassPhraseCacheTtl = null;
+ private IntegerListPreference mEncryptionAlgorithm = null;
+ private IntegerListPreference mHashAlgorithm = null;
+ private IntegerListPreference mMessageCompression = null;
+ private IntegerListPreference mFileCompression = null;
+ private CheckBoxPreference mAsciiArmour = null;
+ private CheckBoxPreference mForceV3Signatures = null;
+ private PreferenceScreen mKeyServerPreference = null;
+ private Preferences mPreferences;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ mPreferences = Preferences.getPreferences(this);
+ super.onCreate(savedInstanceState);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+
+ addPreferencesFromResource(R.xml.preferences);
+
+ mPassPhraseCacheTtl = (IntegerListPreference) findPreference(Constants.pref.PASS_PHRASE_CACHE_TTL);
+ mPassPhraseCacheTtl.setValue("" + mPreferences.getPassPhraseCacheTtl());
+ mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry());
+ mPassPhraseCacheTtl
+ .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mPassPhraseCacheTtl.setValue(newValue.toString());
+ mPassPhraseCacheTtl.setSummary(mPassPhraseCacheTtl.getEntry());
+ mPreferences.setPassPhraseCacheTtl(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+
+ mEncryptionAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_ENCRYPTION_ALGORITHM);
+ int valueIds[] = { PGPEncryptedData.AES_128, PGPEncryptedData.AES_192,
+ PGPEncryptedData.AES_256, PGPEncryptedData.BLOWFISH, PGPEncryptedData.TWOFISH,
+ PGPEncryptedData.CAST5, PGPEncryptedData.DES, PGPEncryptedData.TRIPLE_DES,
+ PGPEncryptedData.IDEA, };
+ String entries[] = { "AES-128", "AES-192", "AES-256", "Blowfish", "Twofish", "CAST5",
+ "DES", "Triple DES", "IDEA", };
+ String values[] = new String[valueIds.length];
+ for (int i = 0; i < values.length; ++i) {
+ values[i] = "" + valueIds[i];
+ }
+ mEncryptionAlgorithm.setEntries(entries);
+ mEncryptionAlgorithm.setEntryValues(values);
+ mEncryptionAlgorithm.setValue("" + mPreferences.getDefaultEncryptionAlgorithm());
+ mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry());
+ mEncryptionAlgorithm
+ .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mEncryptionAlgorithm.setValue(newValue.toString());
+ mEncryptionAlgorithm.setSummary(mEncryptionAlgorithm.getEntry());
+ mPreferences.setDefaultEncryptionAlgorithm(Integer.parseInt(newValue
+ .toString()));
+ return false;
+ }
+ });
+
+ mHashAlgorithm = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_HASH_ALGORITHM);
+ valueIds = new int[] { HashAlgorithmTags.MD5, HashAlgorithmTags.RIPEMD160,
+ HashAlgorithmTags.SHA1, HashAlgorithmTags.SHA224, HashAlgorithmTags.SHA256,
+ HashAlgorithmTags.SHA384, HashAlgorithmTags.SHA512, };
+ entries = new String[] { "MD5", "RIPEMD-160", "SHA-1", "SHA-224", "SHA-256", "SHA-384",
+ "SHA-512", };
+ values = new String[valueIds.length];
+ for (int i = 0; i < values.length; ++i) {
+ values[i] = "" + valueIds[i];
+ }
+ mHashAlgorithm.setEntries(entries);
+ mHashAlgorithm.setEntryValues(values);
+ mHashAlgorithm.setValue("" + mPreferences.getDefaultHashAlgorithm());
+ mHashAlgorithm.setSummary(mHashAlgorithm.getEntry());
+ mHashAlgorithm.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mHashAlgorithm.setValue(newValue.toString());
+ mHashAlgorithm.setSummary(mHashAlgorithm.getEntry());
+ mPreferences.setDefaultHashAlgorithm(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+
+ mMessageCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_MESSAGE_COMPRESSION);
+ valueIds = new int[] { Id.choice.compression.none, Id.choice.compression.zip,
+ Id.choice.compression.zlib, Id.choice.compression.bzip2, };
+ entries = new String[] {
+ getString(R.string.choice_none) + " (" + getString(R.string.fast) + ")",
+ "ZIP (" + getString(R.string.fast) + ")",
+ "ZLIB (" + getString(R.string.fast) + ")",
+ "BZIP2 (" + getString(R.string.very_slow) + ")", };
+ values = new String[valueIds.length];
+ for (int i = 0; i < values.length; ++i) {
+ values[i] = "" + valueIds[i];
+ }
+ mMessageCompression.setEntries(entries);
+ mMessageCompression.setEntryValues(values);
+ mMessageCompression.setValue("" + mPreferences.getDefaultMessageCompression());
+ mMessageCompression.setSummary(mMessageCompression.getEntry());
+ mMessageCompression
+ .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mMessageCompression.setValue(newValue.toString());
+ mMessageCompression.setSummary(mMessageCompression.getEntry());
+ mPreferences.setDefaultMessageCompression(Integer.parseInt(newValue
+ .toString()));
+ return false;
+ }
+ });
+
+ mFileCompression = (IntegerListPreference) findPreference(Constants.pref.DEFAULT_FILE_COMPRESSION);
+ mFileCompression.setEntries(entries);
+ mFileCompression.setEntryValues(values);
+ mFileCompression.setValue("" + mPreferences.getDefaultFileCompression());
+ mFileCompression.setSummary(mFileCompression.getEntry());
+ mFileCompression.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mFileCompression.setValue(newValue.toString());
+ mFileCompression.setSummary(mFileCompression.getEntry());
+ mPreferences.setDefaultFileCompression(Integer.parseInt(newValue.toString()));
+ return false;
+ }
+ });
+
+ mAsciiArmour = (CheckBoxPreference) findPreference(Constants.pref.DEFAULT_ASCII_ARMOUR);
+ mAsciiArmour.setChecked(mPreferences.getDefaultAsciiArmour());
+ mAsciiArmour.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mAsciiArmour.setChecked((Boolean) newValue);
+ mPreferences.setDefaultAsciiArmour((Boolean) newValue);
+ return false;
+ }
+ });
+
+ mForceV3Signatures = (CheckBoxPreference) findPreference(Constants.pref.FORCE_V3_SIGNATURES);
+ mForceV3Signatures.setChecked(mPreferences.getForceV3Signatures());
+ mForceV3Signatures
+ .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ mForceV3Signatures.setChecked((Boolean) newValue);
+ mPreferences.setForceV3Signatures((Boolean) newValue);
+ return false;
+ }
+ });
+
+ mKeyServerPreference = (PreferenceScreen) findPreference(Constants.pref.KEY_SERVERS);
+ String servers[] = mPreferences.getKeyServers();
+ mKeyServerPreference.setSummary(getResources().getString(R.string.nKeyServers,
+ servers.length));
+ mKeyServerPreference
+ .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ Intent intent = new Intent(PreferencesActivity.this,
+ PreferencesKeyServerActivity.class);
+ intent.putExtra(PreferencesKeyServerActivity.EXTRA_KEY_SERVERS,
+ mPreferences.getKeyServers());
+ startActivityForResult(intent, Id.request.key_server_preference);
+ return false;
+ }
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.key_server_preference: {
+ if (resultCode == RESULT_CANCELED || data == null) {
+ return;
+ }
+ String servers[] = data
+ .getStringArrayExtra(PreferencesKeyServerActivity.EXTRA_KEY_SERVERS);
+ mPreferences.setKeyServers(servers);
+ mKeyServerPreference.setSummary(getResources().getString(R.string.nKeyServers,
+ servers.length));
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java
new file mode 100644
index 000000000..b86fe0a2e
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/PreferencesKeyServerActivity.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.util.Vector;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.ui.widget.Editor;
+import org.sufficientlysecure.keychain.ui.widget.KeyServerEditor;
+import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+public class PreferencesKeyServerActivity extends SherlockActivity implements OnClickListener,
+ EditorListener {
+
+ public static final String EXTRA_KEY_SERVERS = "keyServers";
+
+ private LayoutInflater mInflater;
+ private ViewGroup mEditors;
+ private View mAdd;
+ private TextView mTitle;
+ private TextView mSummary;
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, PreferencesActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+
+ return true;
+
+ case Id.menu.option.okay:
+ okClicked();
+
+ return true;
+
+ case Id.menu.option.cancel:
+ cancelClicked();
+
+ return true;
+
+ default:
+ break;
+
+ }
+ return false;
+ }
+
+ /**
+ * ActionBar menu is created based on class variables to change it at runtime
+ *
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+
+ menu.add(1, Id.menu.option.cancel, 0, android.R.string.cancel).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ menu.add(1, Id.menu.option.okay, 1, android.R.string.ok).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+ return true;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.key_server_preference);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+
+ mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ mTitle = (TextView) findViewById(R.id.title);
+ mSummary = (TextView) findViewById(R.id.summary);
+
+ mTitle.setText(R.string.label_keyServers);
+
+ mEditors = (ViewGroup) findViewById(R.id.editors);
+ mAdd = findViewById(R.id.add);
+ mAdd.setOnClickListener(this);
+
+ Intent intent = getIntent();
+ String servers[] = intent.getStringArrayExtra(EXTRA_KEY_SERVERS);
+ if (servers != null) {
+ for (int i = 0; i < servers.length; ++i) {
+ KeyServerEditor view = (KeyServerEditor) mInflater.inflate(
+ R.layout.key_server_editor, mEditors, false);
+ view.setEditorListener(this);
+ view.setValue(servers[i]);
+ mEditors.addView(view);
+ }
+ }
+ }
+
+ public void onDeleted(Editor editor) {
+ // nothing to do
+ }
+
+ public void onClick(View v) {
+ KeyServerEditor view = (KeyServerEditor) mInflater.inflate(R.layout.key_server_editor,
+ mEditors, false);
+ view.setEditorListener(this);
+ mEditors.addView(view);
+ }
+
+ private void cancelClicked() {
+ setResult(RESULT_CANCELED, null);
+ finish();
+ }
+
+ private void okClicked() {
+ Intent data = new Intent();
+ Vector<String> servers = new Vector<String>();
+ for (int i = 0; i < mEditors.getChildCount(); ++i) {
+ KeyServerEditor editor = (KeyServerEditor) mEditors.getChildAt(i);
+ String tmp = editor.getValue();
+ if (tmp.length() > 0) {
+ servers.add(tmp);
+ }
+ }
+ String[] dummy = new String[0];
+ data.putExtra(EXTRA_KEY_SERVERS, servers.toArray(dummy));
+ setResult(RESULT_OK, data);
+ finish();
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java
new file mode 100644
index 000000000..fc1cfba17
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyActivity.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+public class SelectPublicKeyActivity extends SherlockFragmentActivity {
+
+ // Not used in sourcode, but listed in AndroidManifest!
+ public static final String ACTION_SELECT_PUBLIC_KEYS = Constants.INTENT_PREFIX
+ + "SELECT_PUBLIC_KEYS";
+
+ public static final String EXTRA_SELECTED_MASTER_KEY_IDS = "masterKeyIds";
+
+ public static final String RESULT_EXTRA_MASTER_KEY_IDS = "masterKeyIds";
+ public static final String RESULT_EXTRA_USER_IDS = "userIds";
+
+ SelectPublicKeyFragment mSelectFragment;
+
+ long selectedMasterKeyIds[];
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.select_public_key_activity);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+
+ mSelectFragment = (SelectPublicKeyFragment) getSupportFragmentManager().findFragmentById(
+ R.id.select_public_key_fragment);
+
+ // TODO: reimplement!
+ // mFilterLayout = findViewById(R.id.layout_filter);
+ // mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
+ // mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
+ //
+ // mClearFilterButton.setOnClickListener(new OnClickListener() {
+ // public void onClick(View v) {
+ // handleIntent(new Intent());
+ // }
+ // });
+
+ handleIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ handleIntent(intent);
+ }
+
+ private void handleIntent(Intent intent) {
+ // TODO: reimplement!
+
+ // String searchString = null;
+ // if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+ // searchString = intent.getStringExtra(SearchManager.QUERY);
+ // if (searchString != null && searchString.trim().length() == 0) {
+ // searchString = null;
+ // }
+ // }
+
+ // if (searchString == null) {
+ // mFilterLayout.setVisibility(View.GONE);
+ // } else {
+ // mFilterLayout.setVisibility(View.VISIBLE);
+ // mFilterInfo.setText(getString(R.string.filterInfo, searchString));
+ // }
+
+ // preselected master keys
+ selectedMasterKeyIds = intent.getLongArrayExtra(EXTRA_SELECTED_MASTER_KEY_IDS);
+ }
+
+ /**
+ * returns preselected key ids, this is used in the fragment
+ *
+ * @return
+ */
+ public long[] getSelectedMasterKeyIds() {
+ return selectedMasterKeyIds;
+ }
+
+ private void cancelClicked() {
+ setResult(RESULT_CANCELED, null);
+ finish();
+ }
+
+ private void okClicked() {
+ Intent data = new Intent();
+ data.putExtra(RESULT_EXTRA_MASTER_KEY_IDS, mSelectFragment.getSelectedMasterKeyIds());
+ data.putExtra(RESULT_EXTRA_USER_IDS, mSelectFragment.getSelectedUserIds());
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // TODO: reimplement!
+ // menu.add(0, Id.menu.option.search, 0, R.string.menu_search).setIcon(
+ // android.R.drawable.ic_menu_search);
+ menu.add(1, Id.menu.option.cancel, 0, R.string.btn_doNotSave).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ menu.add(1, Id.menu.option.okay, 1, R.string.btn_okay).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+ return true;
+ }
+
+ /**
+ * Menu Options
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case Id.menu.option.okay:
+ okClicked();
+ return true;
+
+ case Id.menu.option.cancel:
+ cancelClicked();
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java
new file mode 100644
index 000000000..8b3e75d05
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectPublicKeyFragment.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.util.Date;
+import java.util.Vector;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.compatibility.ListFragmentWorkaround;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
+import org.sufficientlysecure.keychain.ui.widget.SelectKeyCursorAdapter;
+import org.sufficientlysecure.keychain.R;
+
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.app.LoaderManager;
+import android.widget.ListView;
+
+public class SelectPublicKeyFragment extends ListFragmentWorkaround implements
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ private SelectPublicKeyActivity mActivity;
+ private SelectKeyCursorAdapter mAdapter;
+ private ListView mListView;
+
+ private long mSelectedMasterKeyIds[];
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mActivity = (SelectPublicKeyActivity) getSherlockActivity();
+ mListView = getListView();
+
+ // get selected master key ids, which are given to activity by intent
+ mSelectedMasterKeyIds = mActivity.getSelectedMasterKeyIds();
+
+ mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+ // Give some text to display if there is no data. In a real
+ // application this would come from a resource.
+ setEmptyText(getString(R.string.listEmpty));
+
+ mAdapter = new SelectKeyCursorAdapter(mActivity, mListView, null, Id.type.public_key);
+
+ setListAdapter(mAdapter);
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ /**
+ * Selects items based on master key ids in list view
+ *
+ * @param masterKeyIds
+ */
+ private void preselectMasterKeyIds(long[] masterKeyIds) {
+ if (masterKeyIds != null) {
+ for (int i = 0; i < mListView.getCount(); ++i) {
+ long keyId = mAdapter.getMasterKeyId(i);
+ for (int j = 0; j < masterKeyIds.length; ++j) {
+ if (keyId == masterKeyIds[j]) {
+ mListView.setItemChecked(i, true);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns all selected master key ids
+ *
+ * @return
+ */
+ public long[] getSelectedMasterKeyIds() {
+ // mListView.getCheckedItemIds() would give the row ids of the KeyRings not the master key
+ // ids!
+ Vector<Long> vector = new Vector<Long>();
+ for (int i = 0; i < mListView.getCount(); ++i) {
+ if (mListView.isItemChecked(i)) {
+ vector.add(mAdapter.getMasterKeyId(i));
+ }
+ }
+
+ // convert to long array
+ long[] selectedMasterKeyIds = new long[vector.size()];
+ for (int i = 0; i < vector.size(); ++i) {
+ selectedMasterKeyIds[i] = vector.get(i);
+ }
+
+ return selectedMasterKeyIds;
+ }
+
+ /**
+ * Returns all selected user ids
+ *
+ * @return
+ */
+ public String[] getSelectedUserIds() {
+ Vector<String> userIds = new Vector<String>();
+ for (int i = 0; i < mListView.getCount(); ++i) {
+ if (mListView.isItemChecked(i)) {
+ userIds.add((String) mAdapter.getUserId(i));
+ }
+ }
+
+ // make empty array to not return null
+ String userIdArray[] = new String[0];
+ return userIds.toArray(userIdArray);
+ }
+
+ @Override
+ public Loader<Cursor> 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();
+
+ // These are the rows that we will retrieve.
+ long now = new Date().getTime() / 1000;
+ String[] projection = new String[] {
+ KeyRings._ID,
+ KeyRings.MASTER_KEY_ID,
+ UserIds.USER_ID,
+ "(SELECT COUNT(available_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ + " AS available_keys WHERE available_keys." + Keys.KEY_RING_ROW_ID + " = "
+ + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
+ + " AND available_keys." + Keys.IS_REVOKED + " = '0' AND available_keys."
+ + Keys.CAN_ENCRYPT + " = '1') AS "
+ + SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE,
+ "(SELECT COUNT(valid_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ + " AS valid_keys WHERE valid_keys." + Keys.KEY_RING_ROW_ID + " = "
+ + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + " AND valid_keys."
+ + Keys.IS_REVOKED + " = '0' AND valid_keys." + Keys.CAN_ENCRYPT
+ + " = '1' AND valid_keys." + Keys.CREATION + " <= '" + now + "' AND "
+ + "(valid_keys." + Keys.EXPIRY + " IS NULL OR valid_keys." + Keys.EXPIRY
+ + " >= '" + now + "')) AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, };
+
+ String inMasterKeyList = null;
+ if (mSelectedMasterKeyIds != null && mSelectedMasterKeyIds.length > 0) {
+ inMasterKeyList = KeyRings.MASTER_KEY_ID + " IN (";
+ for (int i = 0; i < mSelectedMasterKeyIds.length; ++i) {
+ if (i != 0) {
+ inMasterKeyList += ", ";
+ }
+ inMasterKeyList += DatabaseUtils.sqlEscapeString("" + mSelectedMasterKeyIds[i]);
+ }
+ inMasterKeyList += ")";
+ }
+
+ // if (searchString != null && searchString.trim().length() > 0) {
+ // String[] chunks = searchString.trim().split(" +");
+ // qb.appendWhere("(EXISTS (SELECT tmp." + UserIds._ID + " FROM " + UserIds.TABLE_NAME
+ // + " AS tmp WHERE " + "tmp." + UserIds.KEY_ID + " = " + Keys.TABLE_NAME + "."
+ // + Keys._ID);
+ // for (int i = 0; i < chunks.length; ++i) {
+ // qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
+ // qb.appendWhereEscapeString("%" + chunks[i] + "%");
+ // }
+ // qb.appendWhere("))");
+ //
+ // if (inIdList != null) {
+ // qb.appendWhere(" OR (" + inIdList + ")");
+ // }
+ // }
+
+ String orderBy = UserIds.USER_ID + " ASC";
+ if (inMasterKeyList != null) {
+ // sort by selected master keys
+ orderBy = inMasterKeyList + " DESC, " + orderBy;
+ }
+
+ // 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, orderBy);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.swapCursor(data);
+
+ // The list should now be shown.
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+
+ // preselect given master keys
+ preselectMasterKeyIds(mSelectedMasterKeyIds);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> 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);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java
new file mode 100644
index 000000000..6f43cbabc
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyActivity.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+public class SelectSecretKeyActivity extends SherlockFragmentActivity {
+
+ // Not used in sourcode, but listed in AndroidManifest!
+ public static final String ACTION_SELECT_SECRET_KEY = Constants.INTENT_PREFIX
+ + "SELECT_SECRET_KEY";
+
+ public static final String RESULT_EXTRA_MASTER_KEY_ID = "masterKeyId";
+ public static final String RESULT_EXTRA_USER_ID = "userId";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.select_secret_key_activity);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+
+ // TODO: reimplement!
+ // mFilterLayout = findViewById(R.id.layout_filter);
+ // mFilterInfo = (TextView) mFilterLayout.findViewById(R.id.filterInfo);
+ // mClearFilterButton = (Button) mFilterLayout.findViewById(R.id.btn_clear);
+ //
+ // mClearFilterButton.setOnClickListener(new OnClickListener() {
+ // public void onClick(View v) {
+ // handleIntent(new Intent());
+ // }
+ // });
+
+ handleIntent(getIntent());
+ }
+
+ /**
+ * This is executed by SelectSecretKeyFragment after clicking on an item
+ *
+ * @param masterKeyId
+ * @param userId
+ */
+ public void afterListSelection(long masterKeyId, String userId) {
+ Intent data = new Intent();
+ data.putExtra(RESULT_EXTRA_MASTER_KEY_ID, masterKeyId);
+ data.putExtra(RESULT_EXTRA_USER_ID, (String) userId);
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ handleIntent(intent);
+ }
+
+ private void handleIntent(Intent intent) {
+ // TODO: reimplement!
+
+ // String searchString = null;
+ // if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+ // searchString = intent.getStringExtra(SearchManager.QUERY);
+ // if (searchString != null && searchString.trim().length() == 0) {
+ // searchString = null;
+ // }
+ // }
+
+ // if (searchString == null) {
+ // mFilterLayout.setVisibility(View.GONE);
+ // } else {
+ // mFilterLayout.setVisibility(View.VISIBLE);
+ // mFilterInfo.setText(getString(R.string.filterInfo, searchString));
+ // }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // TODO: reimplement!
+ // menu.add(0, Id.menu.option.search, 0, R.string.menu_search).setIcon(
+ // android.R.drawable.ic_menu_search);
+ return true;
+ }
+
+ /**
+ * Menu Options
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go home
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java
new file mode 100644
index 000000000..d7d9fb0af
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SelectSecretKeyFragment.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.util.Date;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.provider.KeychainDatabase.Tables;
+import org.sufficientlysecure.keychain.ui.widget.SelectKeyCursorAdapter;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockListFragment;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.app.LoaderManager;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ListView;
+
+public class SelectSecretKeyFragment extends SherlockListFragment implements
+ LoaderManager.LoaderCallbacks<Cursor> {
+
+ private SelectSecretKeyActivity mActivity;
+ private SelectKeyCursorAdapter mAdapter;
+ private ListView mListView;
+
+ /**
+ * Define Adapter and Loader on create of Activity
+ */
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mActivity = (SelectSecretKeyActivity) getSherlockActivity();
+ mListView = getListView();
+
+ mListView.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
+ long masterKeyId = mAdapter.getMasterKeyId(position);
+ String userId = mAdapter.getUserId(position);
+
+ // return data to activity, which results in finishing it
+ mActivity.afterListSelection(masterKeyId, userId);
+ }
+ });
+
+ // Give some text to display if there is no data. In a real
+ // application this would come from a resource.
+ setEmptyText(getString(R.string.listEmpty));
+
+ mAdapter = new SelectKeyCursorAdapter(mActivity, mListView, null, Id.type.secret_key);
+
+ setListAdapter(mAdapter);
+
+ // Start out with a progress indicator.
+ setListShown(false);
+
+ // Prepare the loader. Either re-connect with an existing one,
+ // or start a new one.
+ getLoaderManager().initLoader(0, null, this);
+ }
+
+ @Override
+ public Loader<Cursor> 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();
+
+ // These are the rows that we will retrieve.
+ long now = new Date().getTime() / 1000;
+ String[] projection = new String[] {
+ KeyRings._ID,
+ KeyRings.MASTER_KEY_ID,
+ UserIds.USER_ID,
+ "(SELECT COUNT(available_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ + " AS available_keys WHERE available_keys." + Keys.KEY_RING_ROW_ID + " = "
+ + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID
+ + " AND available_keys." + Keys.IS_REVOKED + " = '0' AND available_keys."
+ + Keys.CAN_SIGN + " = '1') AS "
+ + SelectKeyCursorAdapter.PROJECTION_ROW_AVAILABLE,
+ "(SELECT COUNT(valid_keys." + Keys._ID + ") FROM " + Tables.KEYS
+ + " AS valid_keys WHERE valid_keys." + Keys.KEY_RING_ROW_ID + " = "
+ + KeychainDatabase.Tables.KEY_RINGS + "." + KeyRings._ID + " AND valid_keys."
+ + Keys.IS_REVOKED + " = '0' AND valid_keys." + Keys.CAN_SIGN
+ + " = '1' AND valid_keys." + Keys.CREATION + " <= '" + now + "' AND "
+ + "(valid_keys." + Keys.EXPIRY + " IS NULL OR valid_keys." + Keys.EXPIRY
+ + " >= '" + now + "')) AS " + SelectKeyCursorAdapter.PROJECTION_ROW_VALID, };
+
+ // if (searchString != null && searchString.trim().length() > 0) {
+ // String[] chunks = searchString.trim().split(" +");
+ // qb.appendWhere("EXISTS (SELECT tmp." + UserIds._ID + " FROM " + UserIds.TABLE_NAME
+ // + " AS tmp WHERE " + "tmp." + UserIds.KEY_ID + " = " + Keys.TABLE_NAME + "."
+ // + Keys._ID);
+ // for (int i = 0; i < chunks.length; ++i) {
+ // qb.appendWhere(" AND tmp." + UserIds.USER_ID + " LIKE ");
+ // qb.appendWhereEscapeString("%" + chunks[i] + "%");
+ // }
+ // qb.appendWhere(")");
+ // }
+
+ String orderBy = UserIds.USER_ID + " ASC";
+
+ // 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, orderBy);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ // Swap the new cursor in. (The framework will take care of closing the
+ // old cursor once we return.)
+ mAdapter.swapCursor(data);
+
+ // The list should now be shown.
+ if (isResumed()) {
+ setListShown(true);
+ } else {
+ setListShownNoAnimation(true);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> 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);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareActivity.java
new file mode 100644
index 000000000..29960748b
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareActivity.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.util.ArrayList;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.google.zxing.integration.android.IntentIntegrator;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+public class ShareActivity extends SherlockFragmentActivity {
+ public static final String ACTION_SHARE_KEYRING = Constants.INTENT_PREFIX + "SHARE_KEYRING";
+ public static final String ACTION_SHARE_KEYRING_WITH_QR_CODE = Constants.INTENT_PREFIX
+ + "SHARE_KEYRING_WITH_QR_CODE";
+
+ public static final String EXTRA_MASTER_KEY_ID = "masterKeyId";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ handleActions(getIntent());
+ }
+
+ protected void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ long masterKeyId = extras.getLong(EXTRA_MASTER_KEY_ID);
+
+ // get public keyring as ascii armored string
+ ArrayList<String> keyringArmored = ProviderHelper.getPublicKeyRingsAsArmoredString(this,
+ new long[] { masterKeyId });
+
+ // close this activity
+ finish();
+
+ if (ACTION_SHARE_KEYRING.equals(action)) {
+ // let user choose application
+ Intent sendIntent = new Intent(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, keyringArmored.get(0));
+ sendIntent.setType("text/plain");
+ startActivity(Intent.createChooser(sendIntent,
+ getResources().getText(R.string.shareKeyringWith)));
+ } else if (ACTION_SHARE_KEYRING_WITH_QR_CODE.equals(action)) {
+ // use barcode scanner integration library
+ new IntentIntegrator(this).shareText(keyringArmored.get(0));
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareNfcBeamActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareNfcBeamActivity.java
new file mode 100644
index 000000000..c106614d3
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ShareNfcBeamActivity.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.nightwhistler.htmlspanner.HtmlSpanner;
+import net.nightwhistler.htmlspanner.JellyBeanSpanFixTextView;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcAdapter.CreateNdefMessageCallback;
+import android.nfc.NfcEvent;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.Log;
+import android.widget.Toast;
+import android.nfc.NfcAdapter.OnNdefPushCompleteCallback;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.Settings;
+import android.text.method.LinkMovementMethod;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+public class ShareNfcBeamActivity extends SherlockFragmentActivity implements
+ CreateNdefMessageCallback, OnNdefPushCompleteCallback {
+ public static final String ACTION_SHARE_KEYRING_WITH_NFC = Constants.INTENT_PREFIX
+ + "SHARE_KEYRING_WITH_NFC";
+
+ public static final String EXTRA_MASTER_KEY_ID = "masterKeyId";
+
+ NfcAdapter mNfcAdapter;
+
+ byte[] mSharedKeyringBytes;
+
+ private static final int MESSAGE_SENT = 1;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ Toast.makeText(this,
+ getString(R.string.error) + ": " + getString(R.string.error_jellyBeanNeeded),
+ Toast.LENGTH_LONG).show();
+ finish();
+ } else {
+ // Check for available NFC Adapter
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
+ if (mNfcAdapter == null) {
+ Toast.makeText(this,
+ getString(R.string.error) + ": " + getString(R.string.error_nfcNeeded),
+ Toast.LENGTH_LONG).show();
+ finish();
+ } else {
+ // handle actions after verifying that nfc works...
+ handleActions(getIntent());
+ }
+ }
+ }
+
+ protected void handleActions(Intent intent) {
+ String action = intent.getAction();
+ Bundle extras = intent.getExtras();
+
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ if (ACTION_SHARE_KEYRING_WITH_NFC.equals(action)) {
+ long masterKeyId = extras.getLong(EXTRA_MASTER_KEY_ID);
+
+ // get public keyring as byte array
+ mSharedKeyringBytes = ProviderHelper.getPublicKeyRingsAsByteArray(this,
+ new long[] { masterKeyId });
+
+ // Register callback to set NDEF message
+ mNfcAdapter.setNdefPushMessageCallback(this, this);
+ // Register callback to listen for message-sent success
+ mNfcAdapter.setOnNdefPushCompleteCallback(this, this);
+ }
+ }
+
+ /**
+ * Parses the NDEF Message from the intent and prints to the TextView
+ */
+ void handleActionNdefDiscovered(Intent intent) {
+ Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
+ // only one message sent during the beam
+ NdefMessage msg = (NdefMessage) rawMsgs[0];
+ // record 0 contains the MIME type, record 1 is the AAR, if present
+ byte[] receivedKeyringBytes = msg.getRecords()[0].getPayload();
+
+ Intent importIntent = new Intent(this, ImportKeysActivity.class);
+ importIntent.setAction(ImportKeysActivity.ACTION_IMPORT);
+ importIntent.putExtra(ImportKeysActivity.EXTRA_KEYRING_BYTES, receivedKeyringBytes);
+
+ finish();
+
+ startActivity(importIntent);
+ }
+
+ private void buildView() {
+ // load html from html file from /res/raw
+ InputStream inputStreamText = getResources().openRawResource(R.raw.nfc_beam_share);
+
+ setContentView(R.layout.share_nfc_beam);
+
+ JellyBeanSpanFixTextView text = (JellyBeanSpanFixTextView) findViewById(R.id.nfc_beam_text);
+
+ // load html into textview
+ HtmlSpanner htmlSpanner = new HtmlSpanner();
+ htmlSpanner.setStripExtraWhiteSpace(true);
+ try {
+ text.setText(htmlSpanner.fromHtml(inputStreamText));
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Error while reading raw resources as stream", e);
+ }
+
+ // make links work
+ text.setMovementMethod(LinkMovementMethod.getInstance());
+
+ // no flickering when clicking textview for Android < 4
+ text.setTextColor(getResources().getColor(android.R.color.black));
+
+ // set actionbar without home button if called from another app
+ OtherHelper.setActionBarBackButton(this);
+ }
+
+ /**
+ * Implementation for the CreateNdefMessageCallback interface
+ */
+ @Override
+ public NdefMessage createNdefMessage(NfcEvent event) {
+ /**
+ * When a device receives a push with an AAR in it, the application specified in the AAR is
+ * guaranteed to run. The AAR overrides the tag dispatch system. You can add it back in to
+ * guarantee that this activity starts when receiving a beamed message. For now, this code
+ * uses the tag dispatch system.
+ */
+ NdefMessage msg = new NdefMessage(NdefRecord.createMime(Constants.NFC_MIME,
+ mSharedKeyringBytes), NdefRecord.createApplicationRecord(Constants.PACKAGE_NAME));
+ return msg;
+ }
+
+ /**
+ * Implementation for the OnNdefPushCompleteCallback interface
+ */
+ @Override
+ public void onNdefPushComplete(NfcEvent arg0) {
+ // A handler is needed to send messages to the activity when this
+ // callback occurs, because it happens from a binder thread
+ mHandler.obtainMessage(MESSAGE_SENT).sendToTarget();
+ }
+
+ /** This handler receives a message from onNdefPushComplete */
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_SENT:
+ Toast.makeText(getApplicationContext(), R.string.nfcSuccessfull, Toast.LENGTH_LONG)
+ .show();
+ break;
+ }
+ }
+ };
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // Check to see that the Activity started due to an Android Beam
+ if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
+ handleActionNdefDiscovered(getIntent());
+ } else {
+ // build view only when sending nfc, not when receiving, as it gets directly into Import
+ // activity on receiving
+ buildView();
+ }
+ }
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ // onResume gets called after this to handle the intent
+ setIntent(intent);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getSupportMenuInflater();
+ inflater.inflate(R.menu.nfc_beam, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ // app icon in Action Bar clicked; go to KeyListPublicActivity
+ Intent intent = new Intent(this, KeyListPublicActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ return true;
+
+ case R.id.menu_settings:
+ Intent intentSettings = new Intent(Settings.ACTION_NFCSHARING_SETTINGS);
+ startActivity(intentSettings);
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java
new file mode 100644
index 000000000..21d55aca0
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2011 Senecaso
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui;
+
+import java.util.Iterator;
+
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSignature;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.helper.Preferences;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.ui.dialog.PassphraseDialogFragment;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.ActionBar;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.Spinner;
+import android.widget.Toast;
+
+/**
+ * gpg --sign-key
+ *
+ * signs the specified public key with the specified secret master key
+ */
+public class SignKeyActivity extends SherlockFragmentActivity {
+
+ public static final String EXTRA_KEY_ID = "keyId";
+
+ private long mPubKeyId = 0;
+ private long mMasterKeyId = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // check we havent already signed it
+ setContentView(R.layout.sign_key_layout);
+
+ final ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setHomeButtonEnabled(false);
+
+ final Spinner keyServer = (Spinner) findViewById(R.id.keyServer);
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+ android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
+ .getKeyServers());
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ keyServer.setAdapter(adapter);
+
+ final CheckBox sendKey = (CheckBox) findViewById(R.id.sendKey);
+ if (!sendKey.isChecked()) {
+ keyServer.setEnabled(false);
+ } else {
+ keyServer.setEnabled(true);
+ }
+
+ sendKey.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (!isChecked) {
+ keyServer.setEnabled(false);
+ } else {
+ keyServer.setEnabled(true);
+ }
+ }
+ });
+
+ Button sign = (Button) findViewById(R.id.sign);
+ sign.setEnabled(false); // disabled until the user selects a key to sign with
+ sign.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (mPubKeyId != 0) {
+ initiateSigning();
+ }
+ }
+ });
+
+ mPubKeyId = getIntent().getLongExtra(EXTRA_KEY_ID, 0);
+ if (mPubKeyId == 0) {
+ finish(); // nothing to do if we dont know what key to sign
+ } else {
+ // kick off the SecretKey selection activity so the user chooses which key to sign with
+ // first
+ Intent intent = new Intent(this, SelectSecretKeyActivity.class);
+ startActivityForResult(intent, Id.request.secret_keys);
+ }
+ }
+
+ private void showPassphraseDialog(final long secretKeyId) {
+ // Message is received after passphrase is cached
+ Handler returnHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == PassphraseDialogFragment.MESSAGE_OKAY) {
+ startSigning();
+ }
+ }
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(returnHandler);
+
+ try {
+ PassphraseDialogFragment passphraseDialog = PassphraseDialogFragment.newInstance(this,
+ messenger, secretKeyId);
+
+ passphraseDialog.show(getSupportFragmentManager(), "passphraseDialog");
+ } catch (PgpMain.PgpGeneralException e) {
+ Log.d(Constants.TAG, "No passphrase for this secret key, encrypt directly!");
+ // send message to handler to start encryption directly
+ returnHandler.sendEmptyMessage(PassphraseDialogFragment.MESSAGE_OKAY);
+ }
+ }
+
+ /**
+ * handles the UI bits of the signing process on the UI thread
+ */
+ private void initiateSigning() {
+ PGPPublicKeyRing pubring = ProviderHelper.getPGPPublicKeyRingByMasterKeyId(this, mPubKeyId);
+ if (pubring != null) {
+ // if we have already signed this key, dont bother doing it again
+ boolean alreadySigned = false;
+
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSignature> itr = pubring.getPublicKey(mPubKeyId).getSignatures();
+ while (itr.hasNext()) {
+ PGPSignature sig = itr.next();
+ if (sig.getKeyID() == mMasterKeyId) {
+ alreadySigned = true;
+ break;
+ }
+ }
+
+ if (!alreadySigned) {
+ /*
+ * get the user's passphrase for this key (if required)
+ */
+ String passphrase = PassphraseCacheService.getCachedPassphrase(this, mMasterKeyId);
+ if (passphrase == null) {
+ showPassphraseDialog(mMasterKeyId);
+ return; // bail out; need to wait until the user has entered the passphrase
+ // before trying again
+ } else {
+ startSigning();
+ }
+ } else {
+ Toast.makeText(this, "Key has already been signed", Toast.LENGTH_SHORT).show();
+
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+ }
+
+ /**
+ * kicks off the actual signing process on a background thread
+ */
+ private void startSigning() {
+ // Send all information needed to service to sign key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_SIGN_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ data.putLong(KeychainIntentService.SIGN_KEY_MASTER_KEY_ID, mMasterKeyId);
+ data.putLong(KeychainIntentService.SIGN_KEY_PUB_KEY_ID, mPubKeyId);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after signing is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, R.string.progress_signing,
+ ProgressDialog.STYLE_SPINNER) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+
+ Toast.makeText(SignKeyActivity.this, R.string.keySignSuccess,
+ Toast.LENGTH_SHORT).show();
+
+ // check if we need to send the key to the server or not
+ CheckBox sendKey = (CheckBox) findViewById(R.id.sendKey);
+ if (sendKey.isChecked()) {
+ /*
+ * upload the newly signed key to the key server
+ */
+ uploadKey();
+ } else {
+ finish();
+ }
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+
+ private void uploadKey() {
+ // Send all information needed to service to upload key in other thread
+ Intent intent = new Intent(this, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_UPLOAD_KEYRING);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ data.putLong(KeychainIntentService.UPLOAD_KEY_KEYRING_ROW_ID, mPubKeyId);
+
+ Spinner keyServer = (Spinner) findViewById(R.id.keyServer);
+ String server = (String) keyServer.getSelectedItem();
+ data.putString(KeychainIntentService.UPLOAD_KEY_SERVER, server);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // Message is received after uploading is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(this, R.string.progress_exporting,
+ ProgressDialog.STYLE_HORIZONTAL) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+
+ Toast.makeText(SignKeyActivity.this, R.string.keySendSuccess,
+ Toast.LENGTH_SHORT).show();
+
+ finish();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ saveHandler.showProgressDialog(this);
+
+ // start service with intent
+ startService(intent);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case Id.request.secret_keys: {
+ if (resultCode == RESULT_OK) {
+ mMasterKeyId = data.getLongExtra(EXTRA_KEY_ID, 0);
+
+ // re-enable the sign button so the user can initiate the sign process
+ Button sign = (Button) findViewById(R.id.sign);
+ sign.setEnabled(true);
+ }
+
+ break;
+ }
+
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java
new file mode 100644
index 000000000..defc3bc6c
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteFileDialogFragment.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+import android.widget.Toast;
+
+public class DeleteFileDialogFragment extends DialogFragment {
+ private static final String ARG_DELETE_FILE = "delete_file";
+
+ /**
+ * Creates new instance of this delete file dialog fragment
+ */
+ public static DeleteFileDialogFragment newInstance(String deleteFile) {
+ DeleteFileDialogFragment frag = new DeleteFileDialogFragment();
+ Bundle args = new Bundle();
+
+ args.putString(ARG_DELETE_FILE, deleteFile);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final FragmentActivity activity = getActivity();
+
+ final String deleteFile = getArguments().getString(ARG_DELETE_FILE);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.warning);
+ alert.setMessage(this.getString(R.string.fileDeleteConfirmation, deleteFile));
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ // Send all information needed to service to edit key in other thread
+ Intent intent = new Intent(activity, KeychainIntentService.class);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_DELETE_FILE_SECURELY);
+ data.putString(KeychainIntentService.DELETE_FILE, deleteFile);
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ ProgressDialogFragment deletingDialog = ProgressDialogFragment.newInstance(
+ R.string.progress_deletingSecurely, ProgressDialog.STYLE_HORIZONTAL);
+
+ // Message is received after deleting is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(activity, deletingDialog) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ Toast.makeText(activity, R.string.fileDeleteSuccessful,
+ Toast.LENGTH_SHORT).show();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ // show progress dialog
+ deletingDialog.show(activity.getSupportFragmentManager(), "deletingDialog");
+
+ // start service with intent
+ activity.startService(intent);
+ }
+ });
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ }
+ });
+ alert.setCancelable(true);
+
+ return alert.create();
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java
new file mode 100644
index 000000000..e61003a9b
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/DeleteKeyDialogFragment.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+
+public class DeleteKeyDialogFragment extends DialogFragment {
+ private static final String ARG_MESSENGER = "messenger";
+ private static final String ARG_DELETE_KEY_RING_ROW_ID = "delete_file";
+ private static final String ARG_KEY_TYPE = "key_type";
+
+ public static final int MESSAGE_OKAY = 1;
+
+ private Messenger mMessenger;
+
+ /**
+ * Creates new instance of this delete file dialog fragment
+ */
+ public static DeleteKeyDialogFragment newInstance(Messenger messenger, long deleteKeyRingRowId,
+ int keyType) {
+ DeleteKeyDialogFragment frag = new DeleteKeyDialogFragment();
+ Bundle args = new Bundle();
+
+ args.putParcelable(ARG_MESSENGER, messenger);
+ args.putLong(ARG_DELETE_KEY_RING_ROW_ID, deleteKeyRingRowId);
+ args.putInt(ARG_KEY_TYPE, keyType);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final FragmentActivity activity = getActivity();
+ mMessenger = getArguments().getParcelable(ARG_MESSENGER);
+
+ final long deleteKeyRingRowId = getArguments().getLong(ARG_DELETE_KEY_RING_ROW_ID);
+ final int keyType = getArguments().getInt(ARG_KEY_TYPE);
+
+ // TODO: better way to do this?
+ String userId = activity.getString(R.string.unknownUserId);
+
+ if (keyType == Id.type.public_key) {
+ PGPPublicKeyRing keyRing = ProviderHelper.getPGPPublicKeyRingByRowId(activity,
+ deleteKeyRingRowId);
+ userId = PgpHelper.getMainUserIdSafe(activity, PgpHelper.getMasterKey(keyRing));
+ } else {
+ PGPSecretKeyRing keyRing = ProviderHelper.getPGPSecretKeyRingByRowId(activity,
+ deleteKeyRingRowId);
+ userId = PgpHelper.getMainUserIdSafe(activity, PgpHelper.getMasterKey(keyRing));
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(R.string.warning);
+ builder.setMessage(getString(
+ keyType == Id.type.public_key ? R.string.keyDeletionConfirmation
+ : R.string.secretKeyDeletionConfirmation, userId));
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setPositiveButton(R.string.btn_delete, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ if (keyType == Id.type.public_key) {
+ ProviderHelper.deletePublicKeyRing(activity, deleteKeyRingRowId);
+ } else {
+ ProviderHelper.deleteSecretKeyRing(activity, deleteKeyRingRowId);
+ }
+
+ dismiss();
+
+ sendMessageToHandler(MESSAGE_OKAY);
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ }
+ });
+ return builder.create();
+ }
+
+ /**
+ * Send message back to handler which is initialized in a activity
+ *
+ * @param what
+ * Message integer you want to send
+ */
+ private void sendMessageToHandler(Integer what) {
+ Message msg = Message.obtain();
+ msg.what = what;
+
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
+ } catch (NullPointerException e) {
+ Log.w(Constants.TAG, "Messenger is null!", e);
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java
new file mode 100644
index 000000000..2f9cdb3b1
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/FileDialogFragment.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.FileHelper;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v4.app.DialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+
+public class FileDialogFragment extends DialogFragment {
+ private static final String ARG_MESSENGER = "messenger";
+ private static final String ARG_TITLE = "title";
+ private static final String ARG_MESSAGE = "message";
+ private static final String ARG_DEFAULT_FILE = "default_file";
+ private static final String ARG_CHECKBOX_TEXT = "checkbox_text";
+ private static final String ARG_REQUEST_CODE = "request_code";
+
+ public static final int MESSAGE_OKAY = 1;
+
+ public static final String MESSAGE_DATA_FILENAME = "filename";
+ public static final String MESSAGE_DATA_CHECKED = "checked";
+
+ private Messenger mMessenger;
+
+ /**
+ * Creates new instance of this file dialog fragment
+ */
+ public static FileDialogFragment newInstance(Messenger messenger, String title, String message,
+ String defaultFile, String checkboxText, int requestCode) {
+ FileDialogFragment frag = new FileDialogFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_MESSENGER, messenger);
+
+ args.putString(ARG_TITLE, title);
+ args.putString(ARG_MESSAGE, message);
+ args.putString(ARG_DEFAULT_FILE, defaultFile);
+ args.putString(ARG_CHECKBOX_TEXT, checkboxText);
+ args.putInt(ARG_REQUEST_CODE, requestCode);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+
+ mMessenger = getArguments().getParcelable(ARG_MESSENGER);
+
+ String title = getArguments().getString(ARG_TITLE);
+ String message = getArguments().getString(ARG_MESSAGE);
+ String defaultFile = getArguments().getString(ARG_DEFAULT_FILE);
+ String checkboxText = getArguments().getString(ARG_CHECKBOX_TEXT);
+ final int requestCode = getArguments().getInt(ARG_REQUEST_CODE);
+
+ final EditText mFilename;
+ final ImageButton mBrowse;
+ final CheckBox mCheckBox;
+
+ LayoutInflater inflater = (LayoutInflater) activity
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setTitle(title);
+ alert.setMessage(message);
+
+ View view = inflater.inflate(R.layout.file_dialog, null);
+
+ mFilename = (EditText) view.findViewById(R.id.input);
+ mFilename.setText(defaultFile);
+ mBrowse = (ImageButton) view.findViewById(R.id.btn_browse);
+ mBrowse.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ // only .asc or .gpg files
+ FileHelper.openFile(activity, mFilename.getText().toString(), "text/plain",
+ requestCode);
+ }
+ });
+
+ mCheckBox = (CheckBox) view.findViewById(R.id.checkbox);
+ if (checkboxText == null) {
+ mCheckBox.setEnabled(false);
+ mCheckBox.setVisibility(View.GONE);
+ } else {
+ mCheckBox.setEnabled(true);
+ mCheckBox.setVisibility(View.VISIBLE);
+ mCheckBox.setText(checkboxText);
+ }
+
+ alert.setView(view);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ boolean checked = false;
+ if (mCheckBox.isEnabled()) {
+ checked = mCheckBox.isChecked();
+ }
+
+ // return resulting data back to activity
+ Bundle data = new Bundle();
+ data.putString(MESSAGE_DATA_FILENAME, mFilename.getText().toString());
+ data.putBoolean(MESSAGE_DATA_CHECKED, checked);
+
+ sendMessageToHandler(MESSAGE_OKAY, data);
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ }
+ });
+ return alert.create();
+ }
+
+ /**
+ * Updates filename in dialog, normally called in onActivityResult in activity using the
+ * FileDialog
+ *
+ * @param messageId
+ * @param progress
+ * @param max
+ */
+ public void setFilename(String filename) {
+ AlertDialog dialog = (AlertDialog) getDialog();
+ EditText filenameEditText = (EditText) dialog.findViewById(R.id.input);
+
+ if (filenameEditText != null) {
+ filenameEditText.setText(filename);
+ }
+ }
+
+ /**
+ * Send message back to handler which is initialized in a activity
+ *
+ * @param what
+ * Message integer you want to send
+ */
+ private void sendMessageToHandler(Integer what, Bundle data) {
+ Message msg = Message.obtain();
+ msg.what = what;
+ if (data != null) {
+ msg.setData(data);
+ }
+
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
+ } catch (NullPointerException e) {
+ Log.w(Constants.TAG, "Messenger is null!", e);
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/LookupUnknownKeyDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/LookupUnknownKeyDialogFragment.java
new file mode 100644
index 000000000..17f900552
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/LookupUnknownKeyDialogFragment.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.ui.KeyServerQueryActivity;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v4.app.DialogFragment;
+
+
+public class LookupUnknownKeyDialogFragment extends DialogFragment {
+ private static final String ARG_MESSENGER = "messenger";
+ private static final String ARG_UNKNOWN_KEY_ID = "unknown_key_id";
+
+ public static final int MESSAGE_OKAY = 1;
+ public static final int MESSAGE_CANCEL = 2;
+
+ private Messenger mMessenger;
+
+ /**
+ * Creates new instance of this dialog fragment
+ *
+ * @param messenger
+ * @param unknownKeyId
+ * @return
+ */
+ public static LookupUnknownKeyDialogFragment newInstance(Messenger messenger, long unknownKeyId) {
+ LookupUnknownKeyDialogFragment frag = new LookupUnknownKeyDialogFragment();
+ Bundle args = new Bundle();
+ args.putLong(ARG_UNKNOWN_KEY_ID, unknownKeyId);
+ args.putParcelable(ARG_MESSENGER, messenger);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+
+ final long unknownKeyId = getArguments().getLong(ARG_UNKNOWN_KEY_ID);
+ mMessenger = getArguments().getParcelable(ARG_MESSENGER);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle(R.string.title_unknownSignatureKey);
+ alert.setMessage(getString(R.string.lookupUnknownKey,
+ PgpHelper.getSmallFingerPrint(unknownKeyId)));
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ sendMessageToHandler(MESSAGE_OKAY);
+
+ Intent intent = new Intent(activity, KeyServerQueryActivity.class);
+ intent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID);
+ intent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, unknownKeyId);
+ startActivityForResult(intent, Id.request.look_up_key_id);
+ }
+ });
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ sendMessageToHandler(MESSAGE_CANCEL);
+ }
+ });
+ alert.setCancelable(true);
+ alert.setOnCancelListener(new OnCancelListener() {
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ sendMessageToHandler(MESSAGE_CANCEL);
+ }
+ });
+
+ return alert.create();
+ }
+
+ /**
+ * Send message back to handler which is initialized in a activity
+ *
+ * @param what
+ * Message integer you want to send
+ */
+ private void sendMessageToHandler(Integer what) {
+ Message msg = Message.obtain();
+ msg.what = what;
+
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
+ } catch (NullPointerException e) {
+ Log.w(Constants.TAG, "Messenger is null!", e);
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java
new file mode 100644
index 000000000..504de9d69
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import org.spongycastle.openpgp.PGPException;
+import org.spongycastle.openpgp.PGPPrivateKey;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.spongycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+import org.sufficientlysecure.keychain.helper.PgpMain.PgpGeneralException;
+import org.sufficientlysecure.keychain.provider.ProviderHelper;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v4.app.DialogFragment;
+
+
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+
+public class PassphraseDialogFragment extends DialogFragment implements OnEditorActionListener {
+ private static final String ARG_MESSENGER = "messenger";
+ private static final String ARG_SECRET_KEY_ID = "secret_key_id";
+
+ public static final int MESSAGE_OKAY = 1;
+
+ private Messenger mMessenger;
+ private EditText mPassphraseEditText;
+
+ /**
+ * Creates new instance of this dialog fragment
+ *
+ * @param secretKeyId
+ * secret key id you want to use
+ * @param messenger
+ * to communicate back after caching the passphrase
+ * @return
+ * @throws PgpGeneralException
+ */
+ public static PassphraseDialogFragment newInstance(Context context, Messenger messenger,
+ long secretKeyId) throws PgpGeneralException {
+ // check if secret key has a passphrase
+ if (!(secretKeyId == Id.key.symmetric || secretKeyId == Id.key.none)) {
+ if (!hasPassphrase(context, secretKeyId)) {
+ throw new PgpMain.PgpGeneralException("No passphrase! No passphrase dialog needed!");
+ }
+ }
+
+ PassphraseDialogFragment frag = new PassphraseDialogFragment();
+ Bundle args = new Bundle();
+ args.putLong(ARG_SECRET_KEY_ID, secretKeyId);
+ args.putParcelable(ARG_MESSENGER, messenger);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Checks if key has a passphrase
+ *
+ * @param secretKeyId
+ * @return true if it has a passphrase
+ */
+ private static boolean hasPassphrase(Context context, long secretKeyId) {
+ // check if the key has no passphrase
+ try {
+ PGPSecretKey secretKey = PgpHelper.getMasterKey(ProviderHelper
+ .getPGPSecretKeyRingByKeyId(context, secretKeyId));
+ // PGPSecretKey secretKey =
+ // PGPHelper.getMasterKey(PGPMain.getSecretKeyRing(secretKeyId));
+
+ Log.d(Constants.TAG, "Check if key has no passphrase...");
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(
+ "SC").build("".toCharArray());
+ PGPPrivateKey testKey = secretKey.extractPrivateKey(keyDecryptor);
+ if (testKey != null) {
+ Log.d(Constants.TAG, "Key has no passphrase! Caches empty passphrase!");
+
+ // cache empty passphrase
+ PassphraseCacheService.addCachedPassphrase(context, secretKey.getKeyID(), "");
+
+ return false;
+ }
+ } catch (PGPException e) {
+ // silently catch
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+
+ long secretKeyId = getArguments().getLong(ARG_SECRET_KEY_ID);
+ mMessenger = getArguments().getParcelable(ARG_MESSENGER);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setTitle(R.string.title_authentication);
+
+ final PGPSecretKey secretKey;
+
+ if (secretKeyId == Id.key.symmetric || secretKeyId == Id.key.none) {
+ secretKey = null;
+ alert.setMessage(R.string.passPhraseForSymmetricEncryption);
+ } else {
+ // TODO: by master key id???
+ secretKey = PgpHelper.getMasterKey(ProviderHelper.getPGPSecretKeyRingByMasterKeyId(
+ activity, secretKeyId));
+ // secretKey = PGPHelper.getMasterKey(PGPMain.getSecretKeyRing(secretKeyId));
+
+ if (secretKey == null) {
+ alert.setTitle(R.string.title_keyNotFound);
+ alert.setMessage(getString(R.string.keyNotFound, secretKeyId));
+ alert.setPositiveButton(android.R.string.ok, new OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ }
+ });
+ alert.setCancelable(false);
+ return alert.create();
+ }
+ String userId = PgpHelper.getMainUserIdSafe(activity, secretKey);
+
+ Log.d(Constants.TAG, "User id: '" + userId + "'");
+ alert.setMessage(getString(R.string.passPhraseFor, userId));
+ }
+
+ LayoutInflater inflater = activity.getLayoutInflater();
+ View view = inflater.inflate(R.layout.passphrase, null);
+ alert.setView(view);
+
+ mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ String passPhrase = mPassphraseEditText.getText().toString();
+ long keyId;
+ if (secretKey != null) {
+ try {
+ PBESecretKeyDecryptor keyDecryptor = new JcePBESecretKeyDecryptorBuilder()
+ .setProvider(PgpMain.BOUNCY_CASTLE_PROVIDER_NAME).build(
+ passPhrase.toCharArray());
+ PGPPrivateKey testKey = secretKey.extractPrivateKey(keyDecryptor);
+ if (testKey == null) {
+ Toast.makeText(activity, R.string.error_couldNotExtractPrivateKey,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ } catch (PGPException e) {
+ Toast.makeText(activity, R.string.wrongPassPhrase, Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+ keyId = secretKey.getKeyID();
+ } else {
+ keyId = Id.key.symmetric;
+ }
+
+ // cache the new passphrase
+ Log.d(Constants.TAG, "Everything okay! Caching entered passphrase");
+ PassphraseCacheService.addCachedPassphrase(activity, keyId, passPhrase);
+
+ sendMessageToHandler(MESSAGE_OKAY);
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ }
+ });
+
+ return alert.create();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle arg0) {
+ super.onActivityCreated(arg0);
+
+ // request focus and open soft keyboard
+ mPassphraseEditText.requestFocus();
+ getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+
+ mPassphraseEditText.setOnEditorActionListener(this);
+ }
+
+ /**
+ * Associate the "done" button on the soft keyboard with the okay button in the view
+ */
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (EditorInfo.IME_ACTION_DONE == actionId) {
+ AlertDialog dialog = ((AlertDialog) getDialog());
+ Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+
+ bt.performClick();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Send message back to handler which is initialized in a activity
+ *
+ * @param what
+ * Message integer you want to send
+ */
+ private void sendMessageToHandler(Integer what) {
+ Message msg = Message.obtain();
+ msg.what = what;
+
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
+ } catch (NullPointerException e) {
+ Log.w(Constants.TAG, "Messenger is null!", e);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java
new file mode 100644
index 000000000..69b31419e
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/ProgressDialogFragment.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnKeyListener;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.KeyEvent;
+
+public class ProgressDialogFragment extends DialogFragment {
+ private static final String ARG_MESSAGE_ID = "message_id";
+ private static final String ARG_STYLE = "style";
+
+ /**
+ * Creates new instance of this fragment
+ *
+ * @param id
+ * @return
+ */
+ public static ProgressDialogFragment newInstance(int messageId, int style) {
+ ProgressDialogFragment frag = new ProgressDialogFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARG_MESSAGE_ID, messageId);
+ args.putInt(ARG_STYLE, style);
+
+ frag.setArguments(args);
+ return frag;
+ }
+
+ /**
+ * Updates progress of dialog
+ *
+ * @param messageId
+ * @param progress
+ * @param max
+ */
+ public void setProgress(int messageId, int progress, int max) {
+ setProgress(getString(messageId), progress, max);
+ }
+
+ /**
+ * Updates progress of dialog
+ *
+ * @param messageId
+ * @param progress
+ * @param max
+ */
+ public void setProgress(int progress, int max) {
+ ProgressDialog dialog = (ProgressDialog) getDialog();
+
+ dialog.setProgress(progress);
+ dialog.setMax(max);
+ }
+
+ /**
+ * Updates progress of dialog
+ *
+ * @param messageId
+ * @param progress
+ * @param max
+ */
+ public void setProgress(String message, int progress, int max) {
+ ProgressDialog dialog = (ProgressDialog) getDialog();
+
+ dialog.setMessage(message);
+ dialog.setProgress(progress);
+ dialog.setMax(max);
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Activity activity = getActivity();
+
+ ProgressDialog dialog = new ProgressDialog(activity);
+ dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ dialog.setCancelable(false);
+ dialog.setCanceledOnTouchOutside(false);
+
+ int messageId = getArguments().getInt(ARG_MESSAGE_ID);
+ int style = getArguments().getInt(ARG_STYLE);
+
+ dialog.setMessage(getString(messageId));
+ dialog.setProgressStyle(style);
+
+ // Disable the back button
+ OnKeyListener keyListener = new OnKeyListener() {
+
+ @Override
+ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
+
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ return true;
+ }
+ return false;
+ }
+
+ };
+ dialog.setOnKeyListener(keyListener);
+
+ return dialog;
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java
new file mode 100644
index 000000000..f001e7a7d
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.dialog;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.v4.app.DialogFragment;
+
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.TextView.OnEditorActionListener;
+
+public class SetPassphraseDialogFragment extends DialogFragment implements OnEditorActionListener {
+ private static final String ARG_MESSENGER = "messenger";
+ private static final String ARG_TITLE = "title";
+
+ public static final int MESSAGE_OKAY = 1;
+
+ public static final String MESSAGE_NEW_PASSPHRASE = "new_passphrase";
+
+ private Messenger mMessenger;
+ private EditText mPassphraseEditText;
+ private EditText mPassphraseAgainEditText;
+
+ /**
+ * Creates new instance of this dialog fragment
+ *
+ * @param title
+ * title of dialog
+ * @param messenger
+ * to communicate back after setting the passphrase
+ * @return
+ */
+ public static SetPassphraseDialogFragment newInstance(Messenger messenger, int title) {
+ SetPassphraseDialogFragment frag = new SetPassphraseDialogFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARG_TITLE, title);
+ args.putParcelable(ARG_MESSENGER, messenger);
+
+ frag.setArguments(args);
+
+ return frag;
+ }
+
+ /**
+ * Creates dialog
+ */
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+
+ int title = getArguments().getInt(ARG_TITLE);
+ mMessenger = getArguments().getParcelable(ARG_MESSENGER);
+
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+
+ alert.setTitle(title);
+ alert.setMessage(R.string.enterPassPhraseTwice);
+
+ LayoutInflater inflater = activity.getLayoutInflater();
+ View view = inflater.inflate(R.layout.passphrase_repeat, null);
+ alert.setView(view);
+
+ mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase);
+ mPassphraseAgainEditText = (EditText) view.findViewById(R.id.passphrase_passphrase_again);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+
+ String passPhrase1 = mPassphraseEditText.getText().toString();
+ String passPhrase2 = mPassphraseAgainEditText.getText().toString();
+ if (!passPhrase1.equals(passPhrase2)) {
+ Toast.makeText(
+ activity,
+ getString(R.string.errorMessage,
+ getString(R.string.passPhrasesDoNotMatch)), Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ if (passPhrase1.equals("")) {
+ Toast.makeText(
+ activity,
+ getString(R.string.errorMessage,
+ getString(R.string.passPhraseMustNotBeEmpty)),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // return resulting data back to activity
+ Bundle data = new Bundle();
+ data.putString(MESSAGE_NEW_PASSPHRASE, passPhrase1);
+
+ sendMessageToHandler(MESSAGE_OKAY, data);
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ }
+ });
+
+ return alert.create();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle arg0) {
+ super.onActivityCreated(arg0);
+
+ // request focus and open soft keyboard
+ mPassphraseEditText.requestFocus();
+ getDialog().getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+
+ mPassphraseAgainEditText.setOnEditorActionListener(this);
+ }
+
+ /**
+ * Associate the "done" button on the soft keyboard with the okay button in the view
+ */
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (EditorInfo.IME_ACTION_DONE == actionId) {
+ AlertDialog dialog = ((AlertDialog) getDialog());
+ Button bt = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+
+ bt.performClick();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Send message back to handler which is initialized in a activity
+ *
+ * @param what
+ * Message integer you want to send
+ */
+ private void sendMessageToHandler(Integer what, Bundle data) {
+ Message msg = Message.obtain();
+ msg.what = what;
+ if (data != null) {
+ msg.setData(data);
+ }
+
+ try {
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
+ } catch (NullPointerException e) {
+ Log.w(Constants.TAG, "Messenger is null!", e);
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/DashboardLayout.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/DashboardLayout.java
new file mode 100644
index 000000000..158a271bc
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/DashboardLayout.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Custom layout that arranges children in a grid-like manner, optimizing for even horizontal and
+ * vertical whitespace.
+ */
+public class DashboardLayout extends ViewGroup {
+ private static final int UNEVEN_GRID_PENALTY_MULTIPLIER = 10;
+
+ private int mMaxChildWidth = 0;
+ private int mMaxChildHeight = 0;
+
+ public DashboardLayout(Context context) {
+ super(context, null);
+ }
+
+ public DashboardLayout(Context context, AttributeSet attrs) {
+ super(context, attrs, 0);
+ }
+
+ public DashboardLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ mMaxChildWidth = 0;
+ mMaxChildHeight = 0;
+
+ // Measure once to find the maximum child size.
+
+ int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST);
+ int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.AT_MOST);
+
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ mMaxChildWidth = Math.max(mMaxChildWidth, child.getMeasuredWidth());
+ mMaxChildHeight = Math.max(mMaxChildHeight, child.getMeasuredHeight());
+ }
+
+ // Measure again for each child to be exactly the same size.
+
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxChildWidth, MeasureSpec.EXACTLY);
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxChildHeight, MeasureSpec.EXACTLY);
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+ setMeasuredDimension(resolveSize(mMaxChildWidth, widthMeasureSpec),
+ resolveSize(mMaxChildHeight, heightMeasureSpec));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int width = r - l;
+ int height = b - t;
+
+ final int count = getChildCount();
+
+ // Calculate the number of visible children.
+ int visibleCount = 0;
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+ ++visibleCount;
+ }
+
+ if (visibleCount == 0) {
+ return;
+ }
+
+ // Calculate what number of rows and columns will optimize for even horizontal and
+ // vertical whitespace between items. Start with a 1 x N grid, then try 2 x N, and so on.
+ int bestSpaceDifference = Integer.MAX_VALUE;
+ int spaceDifference;
+
+ // Horizontal and vertical space between items
+ int hSpace = 0;
+ int vSpace = 0;
+
+ int cols = 1;
+ int rows;
+
+ while (true) {
+ rows = (visibleCount - 1) / cols + 1;
+
+ hSpace = ((width - mMaxChildWidth * cols) / (cols + 1));
+ vSpace = ((height - mMaxChildHeight * rows) / (rows + 1));
+
+ spaceDifference = Math.abs(vSpace - hSpace);
+ if (rows * cols != visibleCount) {
+ spaceDifference *= UNEVEN_GRID_PENALTY_MULTIPLIER;
+ } else if (rows * mMaxChildHeight > height || cols * mMaxChildWidth > width) {
+ spaceDifference *= UNEVEN_GRID_PENALTY_MULTIPLIER;
+ }
+
+ if (spaceDifference < bestSpaceDifference) {
+ // Found a better whitespace squareness/ratio
+ bestSpaceDifference = spaceDifference;
+
+ // If we found a better whitespace squareness and there's only 1 row, this is
+ // the best we can do.
+ if (rows == 1) {
+ break;
+ }
+ } else {
+ // This is a worse whitespace ratio, use the previous value of cols and exit.
+ --cols;
+ rows = (visibleCount - 1) / cols + 1;
+ hSpace = ((width - mMaxChildWidth * cols) / (cols + 1));
+ vSpace = ((height - mMaxChildHeight * rows) / (rows + 1));
+ break;
+ }
+
+ ++cols;
+ }
+
+ // Lay out children based on calculated best-fit number of rows and cols.
+
+ // If we chose a layout that has negative horizontal or vertical space, force it to zero.
+ hSpace = Math.max(0, hSpace);
+ vSpace = Math.max(0, vSpace);
+
+ // Re-use width/height variables to be child width/height.
+ width = (width - hSpace * (cols + 1)) / cols;
+ height = (height - vSpace * (rows + 1)) / rows;
+
+ int left, top;
+ int col, row;
+ int visibleIndex = 0;
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ row = visibleIndex / cols;
+ col = visibleIndex % cols;
+
+ left = hSpace * (col + 1) + width * col;
+ top = vSpace * (row + 1) + height * row;
+
+ child.layout(left, top, (hSpace == 0 && col == cols - 1) ? r : (left + width),
+ (vSpace == 0 && row == rows - 1) ? b : (top + height));
+ ++visibleIndex;
+ }
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/Editor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/Editor.java
new file mode 100644
index 000000000..1cf510d3a
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/Editor.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+public interface Editor {
+ public interface EditorListener {
+ public void onDeleted(Editor editor);
+ }
+
+ public void setEditorListener(EditorListener listener);
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ExpandableListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ExpandableListFragment.java
new file mode 100644
index 000000000..54022342f
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ExpandableListFragment.java
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.Fragment;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnCreateContextMenuListener;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+/**
+ * @author Khoa Tran
+ *
+ * @see android.support.v4.app.ListFragment
+ * @see android.app.ExpandableListActivity
+ *
+ * ExpandableListFragment for Android < 3.0
+ *
+ * from
+ * http://stackoverflow.com/questions/6051050/expandablelistfragment-with-loadermanager-for-
+ * compatibility-package
+ *
+ */
+public class ExpandableListFragment extends Fragment implements OnCreateContextMenuListener,
+ ExpandableListView.OnChildClickListener, ExpandableListView.OnGroupCollapseListener,
+ ExpandableListView.OnGroupExpandListener {
+
+ static final int INTERNAL_EMPTY_ID = 0x00ff0001;
+ static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002;
+ static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003;
+
+ final private Handler mHandler = new Handler();
+
+ final private Runnable mRequestFocus = new Runnable() {
+ public void run() {
+ mExpandableList.focusableViewAvailable(mExpandableList);
+ }
+ };
+
+ final private AdapterView.OnItemClickListener mOnClickListener = new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+ onListItemClick((ExpandableListView) parent, v, position, id);
+ }
+ };
+
+ ExpandableListAdapter mAdapter;
+ ExpandableListView mExpandableList;
+ boolean mFinishedStart = false;
+ View mEmptyView;
+ TextView mStandardEmptyView;
+ View mProgressContainer;
+ View mExpandableListContainer;
+ CharSequence mEmptyText;
+ boolean mExpandableListShown;
+
+ public ExpandableListFragment() {
+ }
+
+ /**
+ * Provide default implementation to return a simple list view. Subclasses can override to
+ * replace with their own layout. If doing so, the returned view hierarchy <em>must</em> have a
+ * ListView whose id is {@link android.R.id#list android.R.id.list} and can optionally have a
+ * sibling view id {@link android.R.id#empty android.R.id.empty} that is to be shown when the
+ * list is empty.
+ *
+ * <p>
+ * If you are overriding this method with your own custom content, consider including the
+ * standard layout {@link android.R.layout#list_content} in your layout file, so that you
+ * continue to retain all of the standard behavior of ListFragment. In particular, this is
+ * currently the only way to have the built-in indeterminant progress state be shown.
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final Context context = getActivity();
+
+ FrameLayout root = new FrameLayout(context);
+
+ // ------------------------------------------------------------------
+
+ LinearLayout pframe = new LinearLayout(context);
+ pframe.setId(INTERNAL_PROGRESS_CONTAINER_ID);
+ pframe.setOrientation(LinearLayout.VERTICAL);
+ pframe.setVisibility(View.GONE);
+ pframe.setGravity(Gravity.CENTER);
+
+ ProgressBar progress = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
+ pframe.addView(progress, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+
+ root.addView(pframe, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ // ------------------------------------------------------------------
+
+ FrameLayout lframe = new FrameLayout(context);
+ lframe.setId(INTERNAL_LIST_CONTAINER_ID);
+
+ TextView tv = new TextView(getActivity());
+ tv.setId(INTERNAL_EMPTY_ID);
+ tv.setGravity(Gravity.CENTER);
+ lframe.addView(tv, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ ExpandableListView lv = new ExpandableListView(getActivity());
+ lv.setId(android.R.id.list);
+ lv.setDrawSelectorOnTop(false);
+ lframe.addView(lv, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ root.addView(lframe, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ // ------------------------------------------------------------------
+
+ root.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+
+ return root;
+ }
+
+ /**
+ * Attach to list view once the view hierarchy has been created.
+ */
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ensureList();
+ }
+
+ /**
+ * Detach from list view.
+ */
+ @Override
+ public void onDestroyView() {
+ mHandler.removeCallbacks(mRequestFocus);
+ mExpandableList = null;
+ mExpandableListShown = false;
+ mEmptyView = mProgressContainer = mExpandableListContainer = null;
+ mStandardEmptyView = null;
+ super.onDestroyView();
+ }
+
+ /**
+ * This method will be called when an item in the list is selected. Subclasses should override.
+ * Subclasses can call getListView().getItemAtPosition(position) if they need to access the data
+ * associated with the selected item.
+ *
+ * @param l
+ * The ListView where the click happened
+ * @param v
+ * The view that was clicked within the ListView
+ * @param position
+ * The position of the view in the list
+ * @param id
+ * The row id of the item that was clicked
+ */
+ public void onListItemClick(ExpandableListView l, View v, int position, long id) {
+ }
+
+ /**
+ * Provide the cursor for the list view.
+ */
+ public void setListAdapter(ExpandableListAdapter adapter) {
+ boolean hadAdapter = mAdapter != null;
+ mAdapter = adapter;
+ if (mExpandableList != null) {
+ mExpandableList.setAdapter(adapter);
+ if (!mExpandableListShown && !hadAdapter) {
+ // The list was hidden, and previously didn't have an
+ // adapter. It is now time to show it.
+ setListShown(true, getView().getWindowToken() != null);
+ }
+ }
+ }
+
+ /**
+ * Set the currently selected list item to the specified position with the adapter's data
+ *
+ * @param position
+ */
+ public void setSelection(int position) {
+ ensureList();
+ mExpandableList.setSelection(position);
+ }
+
+ /**
+ * Get the position of the currently selected list item.
+ */
+ public int getSelectedItemPosition() {
+ ensureList();
+ return mExpandableList.getSelectedItemPosition();
+ }
+
+ /**
+ * Get the cursor row ID of the currently selected list item.
+ */
+ public long getSelectedItemId() {
+ ensureList();
+ return mExpandableList.getSelectedItemId();
+ }
+
+ /**
+ * Get the activity's list view widget.
+ */
+ public ExpandableListView getListView() {
+ ensureList();
+ return mExpandableList;
+ }
+
+ /**
+ * The default content for a ListFragment has a TextView that can be shown when the list is
+ * empty. If you would like to have it shown, call this method to supply the text it should use.
+ */
+ public void setEmptyText(CharSequence text) {
+ ensureList();
+ if (mStandardEmptyView == null) {
+ throw new IllegalStateException("Can't be used with a custom content view");
+ }
+ mStandardEmptyView.setText(text);
+ if (mEmptyText == null) {
+ mExpandableList.setEmptyView(mStandardEmptyView);
+ }
+ mEmptyText = text;
+ }
+
+ /**
+ * Control whether the list is being displayed. You can make it not displayed if you are waiting
+ * for the initial data to show in it. During this time an indeterminant progress indicator will
+ * be shown instead.
+ *
+ * <p>
+ * Applications do not normally need to use this themselves. The default behavior of
+ * ListFragment is to start with the list not being shown, only showing it once an adapter is
+ * given with {@link #setListAdapter(ListAdapter)}. If the list at that point had not been
+ * shown, when it does get shown it will be do without the user ever seeing the hidden state.
+ *
+ * @param shown
+ * If true, the list view is shown; if false, the progress indicator. The initial
+ * value is true.
+ */
+ public void setListShown(boolean shown) {
+ setListShown(shown, true);
+ }
+
+ /**
+ * Like {@link #setListShown(boolean)}, but no animation is used when transitioning from the
+ * previous state.
+ */
+ public void setListShownNoAnimation(boolean shown) {
+ setListShown(shown, false);
+ }
+
+ /**
+ * Control whether the list is being displayed. You can make it not displayed if you are waiting
+ * for the initial data to show in it. During this time an indeterminant progress indicator will
+ * be shown instead.
+ *
+ * @param shown
+ * If true, the list view is shown; if false, the progress indicator. The initial
+ * value is true.
+ * @param animate
+ * If true, an animation will be used to transition to the new state.
+ */
+ private void setListShown(boolean shown, boolean animate) {
+ ensureList();
+ if (mProgressContainer == null) {
+ throw new IllegalStateException("Can't be used with a custom content view");
+ }
+ if (mExpandableListShown == shown) {
+ return;
+ }
+ mExpandableListShown = shown;
+ if (shown) {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(),
+ android.R.anim.fade_out));
+ mExpandableListContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(),
+ android.R.anim.fade_in));
+ } else {
+ mProgressContainer.clearAnimation();
+ mExpandableListContainer.clearAnimation();
+ }
+ mProgressContainer.setVisibility(View.GONE);
+ mExpandableListContainer.setVisibility(View.VISIBLE);
+ } else {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(),
+ android.R.anim.fade_in));
+ mExpandableListContainer.startAnimation(AnimationUtils.loadAnimation(getActivity(),
+ android.R.anim.fade_out));
+ } else {
+ mProgressContainer.clearAnimation();
+ mExpandableListContainer.clearAnimation();
+ }
+ mProgressContainer.setVisibility(View.VISIBLE);
+ mExpandableListContainer.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Get the ListAdapter associated with this activity's ListView.
+ */
+ public ExpandableListAdapter getListAdapter() {
+ return mAdapter;
+ }
+
+ private void ensureList() {
+ if (mExpandableList != null) {
+ return;
+ }
+ View root = getView();
+ if (root == null) {
+ throw new IllegalStateException("Content view not yet created");
+ }
+ if (root instanceof ExpandableListView) {
+ mExpandableList = (ExpandableListView) root;
+ } else {
+ mStandardEmptyView = (TextView) root.findViewById(INTERNAL_EMPTY_ID);
+ if (mStandardEmptyView == null) {
+ mEmptyView = root.findViewById(android.R.id.empty);
+ } else {
+ mStandardEmptyView.setVisibility(View.GONE);
+ }
+ mProgressContainer = root.findViewById(INTERNAL_PROGRESS_CONTAINER_ID);
+ mExpandableListContainer = root.findViewById(INTERNAL_LIST_CONTAINER_ID);
+ View rawExpandableListView = root.findViewById(android.R.id.list);
+ if (!(rawExpandableListView instanceof ExpandableListView)) {
+ if (rawExpandableListView == null) {
+ throw new RuntimeException(
+ "Your content must have a ListView whose id attribute is "
+ + "'android.R.id.list'");
+ }
+ throw new RuntimeException(
+ "Content has view with id attribute 'android.R.id.list' "
+ + "that is not a ListView class");
+ }
+ mExpandableList = (ExpandableListView) rawExpandableListView;
+ if (mEmptyView != null) {
+ mExpandableList.setEmptyView(mEmptyView);
+ } else if (mEmptyText != null) {
+ mStandardEmptyView.setText(mEmptyText);
+ mExpandableList.setEmptyView(mStandardEmptyView);
+ }
+ }
+ mExpandableListShown = true;
+ mExpandableList.setOnItemClickListener(mOnClickListener);
+ if (mAdapter != null) {
+ ExpandableListAdapter adapter = mAdapter;
+ mAdapter = null;
+ setListAdapter(adapter);
+ } else {
+ // We are starting without an adapter, so assume we won't
+ // have our data right away and start with the progress indicator.
+ if (mProgressContainer != null) {
+ setListShown(false, false);
+ }
+ }
+ mHandler.post(mRequestFocus);
+ }
+
+ /**
+ * Override this to populate the context menu when an item is long pressed. menuInfo will
+ * contain an {@link android.widget.ExpandableListView.ExpandableListContextMenuInfo} whose
+ * packedPosition is a packed position that should be used with
+ * {@link ExpandableListView#getPackedPositionType(long)} and the other similar methods.
+ * <p>
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ }
+
+ /**
+ * Override this for receiving callbacks when a child has been clicked.
+ * <p>
+ * {@inheritDoc}
+ */
+ public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+ int childPosition, long id) {
+ return false;
+ }
+
+ /**
+ * Override this for receiving callbacks when a group has been collapsed.
+ */
+ public void onGroupCollapse(int groupPosition) {
+ }
+
+ /**
+ * Override this for receiving callbacks when a group has been expanded.
+ */
+ public void onGroupExpand(int groupPosition) {
+ }
+
+ // /**
+ // * Ensures the expandable list view has been created before Activity restores all
+ // * of the view states.
+ // *
+ // *@see Activity#onRestoreInstanceState(Bundle)
+ // */
+ // @Override
+ // protected void onRestoreInstanceState(Bundle state) {
+ // ensureList();
+ // super.onRestoreInstanceState(state);
+ // }
+
+ /**
+ * Updates the screen state (current list and other views) when the content changes.
+ *
+ * @see Activity#onContentChanged()
+ */
+
+ public void onContentChanged() {
+ // super.onContentChanged();
+ View emptyView = getView().findViewById(android.R.id.empty);
+ mExpandableList = (ExpandableListView) getView().findViewById(android.R.id.list);
+ if (mExpandableList == null) {
+ throw new RuntimeException(
+ "Your content must have a ExpandableListView whose id attribute is "
+ + "'android.R.id.list'");
+ }
+ if (emptyView != null) {
+ mExpandableList.setEmptyView(emptyView);
+ }
+ mExpandableList.setOnChildClickListener(this);
+ mExpandableList.setOnGroupExpandListener(this);
+ mExpandableList.setOnGroupCollapseListener(this);
+
+ if (mFinishedStart) {
+ setListAdapter(mAdapter);
+ }
+ mFinishedStart = true;
+ }
+
+ /**
+ * Get the activity's expandable list view widget. This can be used to get the selection, set
+ * the selection, and many other useful functions.
+ *
+ * @see ExpandableListView
+ */
+ public ExpandableListView getExpandableListView() {
+ ensureList();
+ return mExpandableList;
+ }
+
+ /**
+ * Get the ExpandableListAdapter associated with this activity's ExpandableListView.
+ */
+ public ExpandableListAdapter getExpandableListAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Gets the ID of the currently selected group or child.
+ *
+ * @return The ID of the currently selected group or child.
+ */
+ public long getSelectedId() {
+ return mExpandableList.getSelectedId();
+ }
+
+ /**
+ * Gets the position (in packed position representation) of the currently selected group or
+ * child. Use {@link ExpandableListView#getPackedPositionType},
+ * {@link ExpandableListView#getPackedPositionGroup}, and
+ * {@link ExpandableListView#getPackedPositionChild} to unpack the returned packed position.
+ *
+ * @return A packed position representation containing the currently selected group or child's
+ * position and type.
+ */
+ public long getSelectedPosition() {
+ return mExpandableList.getSelectedPosition();
+ }
+
+ /**
+ * Sets the selection to the specified child. If the child is in a collapsed group, the group
+ * will only be expanded and child subsequently selected if shouldExpandGroup is set to true,
+ * otherwise the method will return false.
+ *
+ * @param groupPosition
+ * The position of the group that contains the child.
+ * @param childPosition
+ * The position of the child within the group.
+ * @param shouldExpandGroup
+ * Whether the child's group should be expanded if it is collapsed.
+ * @return Whether the selection was successfully set on the child.
+ */
+ public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) {
+ return mExpandableList.setSelectedChild(groupPosition, childPosition, shouldExpandGroup);
+ }
+
+ /**
+ * Sets the selection to the specified group.
+ *
+ * @param groupPosition
+ * The position of the group that should be selected.
+ */
+ public void setSelectedGroup(int groupPosition) {
+ mExpandableList.setSelectedGroup(groupPosition);
+ }
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ImportKeysListLoader.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ImportKeysListLoader.java
new file mode 100644
index 000000000..198140e0c
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/ImportKeysListLoader.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.spongycastle.openpgp.PGPKeyRing;
+import org.spongycastle.openpgp.PGPObjectFactory;
+import org.spongycastle.openpgp.PGPPublicKeyRing;
+import org.spongycastle.openpgp.PGPPublicKeyRingCollection;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.spongycastle.openpgp.PGPSecretKeyRingCollection;
+import org.spongycastle.openpgp.PGPUtil;
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.helper.PgpConversionHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.util.InputData;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.util.PositionAwareInputStream;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Context;
+import android.support.v4.content.AsyncTaskLoader;
+
+/**
+ * A custom Loader to search for bad adware apps, based on
+ * https://github.com/brosmike/AirPush-Detector. Daniel Bjorge licensed it under Apachev2 after
+ * asking him by mail.
+ */
+public class ImportKeysListLoader extends AsyncTaskLoader<List<Map<String, String>>> {
+ public static final String MAP_ATTR_USER_ID = "user_id";
+ public static final String MAP_ATTR_FINGERPINT = "fingerprint";
+
+ ArrayList<Map<String, String>> data = new ArrayList<Map<String, String>>();
+
+ Context mContext;
+ List<String> mItems;
+
+ byte[] mKeyringBytes;
+ String mImportFilename;
+
+ public ImportKeysListLoader(Context context, byte[] keyringBytes, String importFilename) {
+ super(context);
+ this.mContext = context;
+ this.mKeyringBytes = keyringBytes;
+ this.mImportFilename = importFilename;
+ }
+
+ @Override
+ public List<Map<String, String>> loadInBackground() {
+ InputData inputData = null;
+ if (mKeyringBytes != null) {
+ inputData = new InputData(new ByteArrayInputStream(mKeyringBytes), mKeyringBytes.length);
+ } else {
+ try {
+ inputData = new InputData(new FileInputStream(mImportFilename),
+ mImportFilename.length());
+ } catch (FileNotFoundException e) {
+ Log.e(Constants.TAG, "Failed to init FileInputStream!", e);
+ }
+ }
+
+ generateListOfKeyrings(inputData);
+
+ return data;
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+
+ // Ensure the loader is stopped
+ onStopLoading();
+ }
+
+ @Override
+ protected void onStartLoading() {
+ forceLoad();
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ public void deliverResult(List<Map<String, String>> data) {
+ super.deliverResult(data);
+ }
+
+ /**
+ * Similar to PGPMain.importKeyRings
+ *
+ * @param keyringBytes
+ * @return
+ */
+ private void generateListOfKeyrings(InputData inputData) {
+ PositionAwareInputStream progressIn = new PositionAwareInputStream(
+ inputData.getInputStream());
+
+ // need to have access to the bufferedInput, so we can reuse it for the possible
+ // PGPObject chunks after the first one, e.g. files with several consecutive ASCII
+ // armour blocks
+ BufferedInputStream bufferedInput = new BufferedInputStream(progressIn);
+ try {
+
+ // read all available blocks... (asc files can contain many blocks with BEGIN END)
+ while (bufferedInput.available() > 0) {
+ InputStream in = PGPUtil.getDecoderStream(bufferedInput);
+ PGPObjectFactory objectFactory = new PGPObjectFactory(in);
+
+ // go through all objects in this block
+ Object obj;
+ while ((obj = objectFactory.nextObject()) != null) {
+ Log.d(Constants.TAG, "Found class: " + obj.getClass());
+
+ if (obj instanceof PGPKeyRing) {
+ PGPKeyRing newKeyring = (PGPKeyRing) obj;
+ addToData(newKeyring);
+ } else {
+ Log.e(Constants.TAG, "Object not recognized as PGPKeyRing!");
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.e(Constants.TAG, "Exception on parsing key file!", e);
+ }
+ }
+
+ private void addToData(PGPKeyRing keyring) {
+ String userId = PgpHelper.getMainUserId(keyring.getPublicKey());
+
+ if (keyring instanceof PGPSecretKeyRing) {
+ userId = mContext.getString(R.string.secretKeyring) + " " + userId;
+ }
+
+ String fingerprint = PgpHelper.convertFingerprintToHex(keyring.getPublicKey()
+ .getFingerprint());
+
+ Map<String, String> attrs = new HashMap<String, String>();
+ attrs.put(MAP_ATTR_USER_ID, userId);
+ attrs.put(MAP_ATTR_FINGERPINT, mContext.getString(R.string.fingerprint) + "\n"
+ + fingerprint);
+ data.add(attrs);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java
new file mode 100644
index 000000000..15b4ddc6a
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/IntegerListPreference.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+
+/**
+ * A list preference which persists its values as integers instead of strings.
+ * Code reading the values should use
+ * {@link android.content.SharedPreferences#getInt}.
+ * When using XML-declared arrays for entry values, the arrays should be regular
+ * string arrays containing valid integer values.
+ *
+ * @author Rodrigo Damazio
+ */
+public class IntegerListPreference extends ListPreference {
+
+ public IntegerListPreference(Context context) {
+ super(context);
+
+ verifyEntryValues(null);
+ }
+
+ public IntegerListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ verifyEntryValues(null);
+ }
+
+ @Override
+ public void setEntryValues(CharSequence[] entryValues) {
+ CharSequence[] oldValues = getEntryValues();
+ super.setEntryValues(entryValues);
+ verifyEntryValues(oldValues);
+ }
+
+ @Override
+ public void setEntryValues(int entryValuesResId) {
+ CharSequence[] oldValues = getEntryValues();
+ super.setEntryValues(entryValuesResId);
+ verifyEntryValues(oldValues);
+ }
+
+ @Override
+ protected String getPersistedString(String defaultReturnValue) {
+ // During initial load, there's no known default value
+ int defaultIntegerValue = Integer.MIN_VALUE;
+ if (defaultReturnValue != null) {
+ defaultIntegerValue = Integer.parseInt(defaultReturnValue);
+ }
+
+ // When the list preference asks us to read a string, instead read an
+ // integer.
+ int value = getPersistedInt(defaultIntegerValue);
+ return Integer.toString(value);
+ }
+
+ @Override
+ protected boolean persistString(String value) {
+ // When asked to save a string, instead save an integer
+ return persistInt(Integer.parseInt(value));
+ }
+
+ private void verifyEntryValues(CharSequence[] oldValues) {
+ CharSequence[] entryValues = getEntryValues();
+ if (entryValues == null) {
+ return;
+ }
+
+ for (CharSequence entryValue : entryValues) {
+ try {
+ Integer.parseInt(entryValue.toString());
+ } catch (NumberFormatException nfe) {
+ super.setEntryValues(oldValues);
+ throw nfe;
+ }
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java
new file mode 100644
index 000000000..1122fc522
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyEditor.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import org.spongycastle.openpgp.PGPPublicKey;
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.util.Choice;
+import org.sufficientlysecure.keychain.R;
+
+import android.app.DatePickerDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.DatePicker;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Vector;
+
+public class KeyEditor extends LinearLayout implements Editor, OnClickListener {
+ private PGPSecretKey mKey;
+
+ private EditorListener mEditorListener = null;
+
+ private boolean mIsMasterKey;
+ ImageButton mDeleteButton;
+ TextView mAlgorithm;
+ TextView mKeyId;
+ Spinner mUsage;
+ TextView mCreationDate;
+ Button mExpiryDateButton;
+ GregorianCalendar mExpiryDate;
+
+ private DatePickerDialog.OnDateSetListener mExpiryDateSetListener = new DatePickerDialog.OnDateSetListener() {
+ public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
+ GregorianCalendar date = new GregorianCalendar(year, monthOfYear, dayOfMonth);
+ setExpiryDate(date);
+ }
+ };
+
+ public KeyEditor(Context context) {
+ super(context);
+ }
+
+ public KeyEditor(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mAlgorithm = (TextView) findViewById(R.id.algorithm);
+ mKeyId = (TextView) findViewById(R.id.keyId);
+ mCreationDate = (TextView) findViewById(R.id.creation);
+ mExpiryDateButton = (Button) findViewById(R.id.expiry);
+ mUsage = (Spinner) findViewById(R.id.usage);
+ Choice choices[] = {
+ new Choice(Id.choice.usage.sign_only, getResources().getString(
+ R.string.choice_signOnly)),
+ new Choice(Id.choice.usage.encrypt_only, getResources().getString(
+ R.string.choice_encryptOnly)),
+ new Choice(Id.choice.usage.sign_and_encrypt, getResources().getString(
+ R.string.choice_signAndEncrypt)), };
+ ArrayAdapter<Choice> adapter = new ArrayAdapter<Choice>(getContext(),
+ android.R.layout.simple_spinner_item, choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mUsage.setAdapter(adapter);
+
+ mDeleteButton = (ImageButton) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+
+ setExpiryDate(null);
+
+ mExpiryDateButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ GregorianCalendar date = mExpiryDate;
+ if (date == null) {
+ date = new GregorianCalendar();
+ }
+
+ DatePickerDialog dialog = new DatePickerDialog(getContext(),
+ mExpiryDateSetListener, date.get(Calendar.YEAR), date.get(Calendar.MONTH),
+ date.get(Calendar.DAY_OF_MONTH));
+ dialog.setCancelable(true);
+ dialog.setButton(Dialog.BUTTON_NEGATIVE, getContext()
+ .getString(R.string.btn_noDate), new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ setExpiryDate(null);
+ }
+ });
+ dialog.show();
+ }
+ });
+
+ super.onFinishInflate();
+ }
+
+ public void setValue(PGPSecretKey key, boolean isMasterKey, int usage) {
+ mKey = key;
+
+ mIsMasterKey = isMasterKey;
+ if (mIsMasterKey) {
+ mDeleteButton.setVisibility(View.INVISIBLE);
+ }
+
+ mAlgorithm.setText(PgpHelper.getAlgorithmInfo(key));
+ String keyId1Str = PgpHelper.getSmallFingerPrint(key.getKeyID());
+ String keyId2Str = PgpHelper.getSmallFingerPrint(key.getKeyID() >> 32);
+ mKeyId.setText(keyId1Str + " " + keyId2Str);
+
+ Vector<Choice> choices = new Vector<Choice>();
+ boolean isElGamalKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT);
+ boolean isDSAKey = (key.getPublicKey().getAlgorithm() == PGPPublicKey.DSA);
+ if (!isElGamalKey) {
+ choices.add(new Choice(Id.choice.usage.sign_only, getResources().getString(
+ R.string.choice_signOnly)));
+ }
+ if (!mIsMasterKey && !isDSAKey) {
+ choices.add(new Choice(Id.choice.usage.encrypt_only, getResources().getString(
+ R.string.choice_encryptOnly)));
+ }
+ if (!isElGamalKey && !isDSAKey) {
+ choices.add(new Choice(Id.choice.usage.sign_and_encrypt, getResources().getString(
+ R.string.choice_signAndEncrypt)));
+ }
+
+ ArrayAdapter<Choice> adapter = new ArrayAdapter<Choice>(getContext(),
+ android.R.layout.simple_spinner_item, choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mUsage.setAdapter(adapter);
+
+ // Set value in choice dropdown to key
+ int selectId = 0;
+ if (PgpHelper.isEncryptionKey(key)) {
+ if (PgpHelper.isSigningKey(key)) {
+ selectId = Id.choice.usage.sign_and_encrypt;
+ } else {
+ selectId = Id.choice.usage.encrypt_only;
+ }
+ } else {
+ // set usage if it is predefined
+ if (usage != -1) {
+ selectId = usage;
+ } else {
+ selectId = Id.choice.usage.sign_only;
+ }
+
+ }
+
+ for (int i = 0; i < choices.size(); ++i) {
+ if (choices.get(i).getId() == selectId) {
+ mUsage.setSelection(i);
+ break;
+ }
+ }
+
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.setTime(PgpHelper.getCreationDate(key));
+ mCreationDate.setText(DateFormat.getDateInstance().format(cal.getTime()));
+ cal = new GregorianCalendar();
+ Date date = PgpHelper.getExpiryDate(key);
+ if (date == null) {
+ setExpiryDate(null);
+ } else {
+ cal.setTime(PgpHelper.getExpiryDate(key));
+ setExpiryDate(cal);
+ }
+
+ }
+
+ public PGPSecretKey getValue() {
+ return mKey;
+ }
+
+ public void onClick(View v) {
+ final ViewGroup parent = (ViewGroup) getParent();
+ if (v == mDeleteButton) {
+ parent.removeView(this);
+ if (mEditorListener != null) {
+ mEditorListener.onDeleted(this);
+ }
+ }
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+
+ private void setExpiryDate(GregorianCalendar date) {
+ mExpiryDate = date;
+ if (date == null) {
+ mExpiryDateButton.setText(R.string.none);
+ } else {
+ mExpiryDateButton.setText(DateFormat.getDateInstance().format(date.getTime()));
+ }
+ }
+
+ public GregorianCalendar getExpiryDate() {
+ return mExpiryDate;
+ }
+
+ public int getUsage() {
+ return ((Choice) mUsage.getSelectedItem()).getId();
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyListAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyListAdapter.java
new file mode 100644
index 000000000..8e8d33886
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyListAdapter.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2012-2013 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import org.sufficientlysecure.keychain.Constants;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.Keys;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.util.Log;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.MergeCursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorTreeAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class KeyListAdapter extends CursorTreeAdapter {
+ private Context mContext;
+ private LayoutInflater mInflater;
+
+ protected int mKeyType;
+
+ private static final int CHILD_KEY = 0;
+ private static final int CHILD_USER_ID = 1;
+ private static final int CHILD_FINGERPRINT = 2;
+
+ public KeyListAdapter(Context context, Cursor groupCursor, int keyType) {
+ super(groupCursor, context);
+ mContext = context;
+ mInflater = LayoutInflater.from(context);
+ mKeyType = keyType;
+ }
+
+ /**
+ * Inflate new view for group items
+ */
+ @Override
+ public View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) {
+ return mInflater.inflate(R.layout.key_list_group_item, null);
+ }
+
+ /**
+ * Binds TextViews from group view to results from database group cursor.
+ */
+ @Override
+ protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
+ int userIdIndex = cursor.getColumnIndex(UserIds.USER_ID);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mainUserId.setText(R.string.unknownUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+
+ String userId = cursor.getString(userIdIndex);
+ if (userId != null) {
+ String[] userIdSplit = OtherHelper.splitUserId(userId);
+
+ if (userIdSplit[1] != null) {
+ mainUserIdRest.setText(userIdSplit[1]);
+ }
+ mainUserId.setText(userIdSplit[0]);
+ }
+
+ if (mainUserId.getText().length() == 0) {
+ mainUserId.setText(R.string.unknownUserId);
+ }
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Inflate new view for child items
+ */
+ @Override
+ public View newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) {
+ return mInflater.inflate(R.layout.key_list_child_item, null);
+ }
+
+ /**
+ * Bind TextViews from view of childs based on query results
+ */
+ @Override
+ protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
+ LinearLayout keyLayout = (LinearLayout) view.findViewById(R.id.keyLayout);
+ LinearLayout userIdLayout = (LinearLayout) view.findViewById(R.id.userIdLayout);
+
+ // first entry is fingerprint
+ if (cursor.getPosition() == 0) {
+ // show only userId layout
+ keyLayout.setVisibility(View.GONE);
+ userIdLayout.setVisibility(View.VISIBLE);
+
+ String fingerprint = PgpHelper.getFingerPrint(context,
+ cursor.getLong(cursor.getColumnIndex(Keys.KEY_ID)));
+ fingerprint = fingerprint.replace(" ", "\n");
+
+ TextView userId = (TextView) view.findViewById(R.id.userId);
+ if (userId == null) {
+ Log.d(Constants.TAG, "userId is null!");
+ }
+ userId.setText(context.getString(R.string.fingerprint) + "\n" + fingerprint);
+ } else {
+ // differentiate between keys and userIds in MergeCursor
+ if (cursor.getColumnIndex(Keys.KEY_ID) != -1) {
+ keyLayout.setVisibility(View.VISIBLE);
+ userIdLayout.setVisibility(View.GONE);
+
+ String keyIdStr = PgpHelper.getSmallFingerPrint(cursor.getLong(cursor
+ .getColumnIndex(Keys.KEY_ID)));
+ String algorithmStr = PgpHelper.getAlgorithmInfo(
+ cursor.getInt(cursor.getColumnIndex(Keys.ALGORITHM)),
+ cursor.getInt(cursor.getColumnIndex(Keys.KEY_SIZE)));
+
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ keyId.setText(keyIdStr);
+
+ TextView keyDetails = (TextView) view.findViewById(R.id.keyDetails);
+ keyDetails.setText("(" + algorithmStr + ")");
+
+ ImageView masterKeyIcon = (ImageView) view.findViewById(R.id.ic_masterKey);
+ if (cursor.getInt(cursor.getColumnIndex(Keys.IS_MASTER_KEY)) != 1) {
+ masterKeyIcon.setVisibility(View.INVISIBLE);
+ } else {
+ masterKeyIcon.setVisibility(View.VISIBLE);
+ }
+
+ ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encryptKey);
+ if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_ENCRYPT)) != 1) {
+ encryptIcon.setVisibility(View.GONE);
+ } else {
+ encryptIcon.setVisibility(View.VISIBLE);
+ }
+
+ ImageView signIcon = (ImageView) view.findViewById(R.id.ic_signKey);
+ if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_SIGN)) != 1) {
+ signIcon.setVisibility(View.GONE);
+ } else {
+ signIcon.setVisibility(View.VISIBLE);
+ }
+ } else {
+ keyLayout.setVisibility(View.GONE);
+ userIdLayout.setVisibility(View.VISIBLE);
+
+ String userIdStr = cursor.getString(cursor.getColumnIndex(UserIds.USER_ID));
+
+ TextView userId = (TextView) view.findViewById(R.id.userId);
+ userId.setText(userIdStr);
+ }
+ }
+ }
+
+ /**
+ * Given the group cursor, we start cursors for a fingerprint, keys, and userIds, which are
+ * merged together and build the child cursor
+ */
+ @Override
+ protected Cursor getChildrenCursor(Cursor groupCursor) {
+ final long keyRingRowId = groupCursor.getLong(groupCursor.getColumnIndex(BaseColumns._ID));
+
+ Cursor fingerprintCursor = getChildCursor(keyRingRowId, CHILD_FINGERPRINT);
+ Cursor keyCursor = getChildCursor(keyRingRowId, CHILD_KEY);
+ Cursor userIdCursor = getChildCursor(keyRingRowId, CHILD_USER_ID);
+
+ MergeCursor mergeCursor = new MergeCursor(new Cursor[] { fingerprintCursor, keyCursor,
+ userIdCursor });
+ Log.d(Constants.TAG, "mergeCursor:" + DatabaseUtils.dumpCursorToString(mergeCursor));
+
+ return mergeCursor;
+ }
+
+ /**
+ * This builds a cursor for a specific type of children
+ *
+ * @param keyRingRowId
+ * foreign row id of the keyRing
+ * @param type
+ * @return
+ */
+ private Cursor getChildCursor(long keyRingRowId, int type) {
+ Uri uri = null;
+ String[] projection = null;
+ String sortOrder = null;
+ String selection = null;
+
+ switch (type) {
+ case CHILD_FINGERPRINT:
+ projection = new String[] { Keys._ID, Keys.KEY_ID, Keys.IS_MASTER_KEY, Keys.ALGORITHM,
+ Keys.KEY_SIZE, Keys.CAN_SIGN, Keys.CAN_ENCRYPT, };
+ sortOrder = Keys.RANK + " ASC";
+
+ // use only master key for fingerprint
+ selection = Keys.IS_MASTER_KEY + " = 1 ";
+
+ if (mKeyType == Id.type.public_key) {
+ uri = Keys.buildPublicKeysUri(String.valueOf(keyRingRowId));
+ } else {
+ uri = Keys.buildSecretKeysUri(String.valueOf(keyRingRowId));
+ }
+ break;
+
+ case CHILD_KEY:
+ projection = new String[] { Keys._ID, Keys.KEY_ID, Keys.IS_MASTER_KEY, Keys.ALGORITHM,
+ Keys.KEY_SIZE, Keys.CAN_SIGN, Keys.CAN_ENCRYPT, };
+ sortOrder = Keys.RANK + " ASC";
+
+ if (mKeyType == Id.type.public_key) {
+ uri = Keys.buildPublicKeysUri(String.valueOf(keyRingRowId));
+ } else {
+ uri = Keys.buildSecretKeysUri(String.valueOf(keyRingRowId));
+ }
+
+ break;
+
+ case CHILD_USER_ID:
+ projection = new String[] { UserIds._ID, UserIds.USER_ID, UserIds.RANK, };
+ sortOrder = UserIds.RANK + " ASC";
+
+ // not the main user id
+ selection = UserIds.RANK + " > 0 ";
+
+ if (mKeyType == Id.type.public_key) {
+ uri = UserIds.buildPublicUserIdsUri(String.valueOf(keyRingRowId));
+ } else {
+ uri = UserIds.buildSecretUserIdsUri(String.valueOf(keyRingRowId));
+ }
+
+ break;
+
+ default:
+ return null;
+
+ }
+
+ return mContext.getContentResolver().query(uri, projection, selection, null, sortOrder);
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java
new file mode 100644
index 000000000..7c9ba5fe6
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/KeyServerEditor.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class KeyServerEditor extends LinearLayout implements Editor, OnClickListener {
+ private EditorListener mEditorListener = null;
+
+ ImageButton mDeleteButton;
+ TextView mServer;
+
+ public KeyServerEditor(Context context) {
+ super(context);
+ }
+
+ public KeyServerEditor(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mServer = (TextView) findViewById(R.id.server);
+
+ mDeleteButton = (ImageButton) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+
+ super.onFinishInflate();
+ }
+
+ public void setValue(String value) {
+ mServer.setText(value);
+ }
+
+ public String getValue() {
+ return mServer.getText().toString().trim();
+ }
+
+ public void onClick(View v) {
+ final ViewGroup parent = (ViewGroup)getParent();
+ if (v == mDeleteButton) {
+ parent.removeView(this);
+ if (mEditorListener != null) {
+ mEditorListener.onDeleted(this);
+ }
+ }
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java
new file mode 100644
index 000000000..7142e4426
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import org.spongycastle.openpgp.PGPSecretKey;
+import org.spongycastle.openpgp.PGPSecretKeyRing;
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.PgpConversionHelper;
+import org.sufficientlysecure.keychain.service.KeychainIntentService;
+import org.sufficientlysecure.keychain.service.KeychainIntentServiceHandler;
+import org.sufficientlysecure.keychain.service.PassphraseCacheService;
+import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment;
+import org.sufficientlysecure.keychain.ui.widget.Editor.EditorListener;
+import org.sufficientlysecure.keychain.util.Choice;
+import org.sufficientlysecure.keychain.R;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.util.Iterator;
+import java.util.Vector;
+
+public class SectionView extends LinearLayout implements OnClickListener, EditorListener {
+ private LayoutInflater mInflater;
+ private View mAdd;
+ private ViewGroup mEditors;
+ private TextView mTitle;
+ private int mType = 0;
+
+ private Choice mNewKeyAlgorithmChoice;
+ private int mNewKeySize;
+
+ private SherlockFragmentActivity mActivity;
+
+ private ProgressDialogFragment mGeneratingDialog;
+
+ public SectionView(Context context) {
+ super(context);
+ mActivity = (SherlockFragmentActivity) context;
+ }
+
+ public SectionView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mActivity = (SherlockFragmentActivity) context;
+ }
+
+ public ViewGroup getEditors() {
+ return mEditors;
+ }
+
+ public void setType(int type) {
+ mType = type;
+ switch (type) {
+ case Id.type.user_id: {
+ mTitle.setText(R.string.section_userIds);
+ break;
+ }
+
+ case Id.type.key: {
+ mTitle.setText(R.string.section_keys);
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onFinishInflate() {
+ mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mAdd = findViewById(R.id.header);
+ mAdd.setOnClickListener(this);
+
+ mEditors = (ViewGroup) findViewById(R.id.editors);
+ mTitle = (TextView) findViewById(R.id.title);
+
+ updateEditorsVisible();
+ super.onFinishInflate();
+ }
+
+ /** {@inheritDoc} */
+ public void onDeleted(Editor editor) {
+ this.updateEditorsVisible();
+ }
+
+ protected void updateEditorsVisible() {
+ final boolean hasChildren = mEditors.getChildCount() > 0;
+ mEditors.setVisibility(hasChildren ? View.VISIBLE : View.GONE);
+ }
+
+ /** {@inheritDoc} */
+ public void onClick(View v) {
+ switch (mType) {
+ case Id.type.user_id: {
+ UserIdEditor view = (UserIdEditor) mInflater.inflate(R.layout.edit_key_user_id_item,
+ mEditors, false);
+ view.setEditorListener(this);
+ if (mEditors.getChildCount() == 0) {
+ view.setIsMainUserId(true);
+ }
+ mEditors.addView(view);
+ break;
+ }
+
+ case Id.type.key: {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
+
+ View view = mInflater.inflate(R.layout.create_key, null);
+ dialog.setView(view);
+ dialog.setTitle(R.string.title_createKey);
+
+ boolean wouldBeMasterKey = (mEditors.getChildCount() == 0);
+
+ final Spinner algorithm = (Spinner) view.findViewById(R.id.create_key_algorithm);
+ Vector<Choice> choices = new Vector<Choice>();
+ choices.add(new Choice(Id.choice.algorithm.dsa, getResources().getString(R.string.dsa)));
+ if (!wouldBeMasterKey) {
+ choices.add(new Choice(Id.choice.algorithm.elgamal, getResources().getString(
+ R.string.elgamal)));
+ }
+
+ choices.add(new Choice(Id.choice.algorithm.rsa, getResources().getString(R.string.rsa)));
+
+ ArrayAdapter<Choice> adapter = new ArrayAdapter<Choice>(getContext(),
+ android.R.layout.simple_spinner_item, choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ algorithm.setAdapter(adapter);
+ // make RSA the default
+ for (int i = 0; i < choices.size(); ++i) {
+ if (choices.get(i).getId() == Id.choice.algorithm.rsa) {
+ algorithm.setSelection(i);
+ break;
+ }
+ }
+
+ final EditText keySize = (EditText) view.findViewById(R.id.create_key_size);
+
+ dialog.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface di, int id) {
+ di.dismiss();
+ try {
+ mNewKeySize = Integer.parseInt("" + keySize.getText());
+ } catch (NumberFormatException e) {
+ mNewKeySize = 0;
+ }
+
+ mNewKeyAlgorithmChoice = (Choice) algorithm.getSelectedItem();
+ createKey();
+ }
+ });
+
+ dialog.setCancelable(true);
+ dialog.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface di, int id) {
+ di.dismiss();
+ }
+ });
+
+ dialog.create().show();
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ this.updateEditorsVisible();
+ }
+
+ public void setUserIds(Vector<String> list) {
+ if (mType != Id.type.user_id) {
+ return;
+ }
+
+ mEditors.removeAllViews();
+ for (String userId : list) {
+ UserIdEditor view = (UserIdEditor) mInflater.inflate(R.layout.edit_key_user_id_item,
+ mEditors, false);
+ view.setEditorListener(this);
+ view.setValue(userId);
+ if (mEditors.getChildCount() == 0) {
+ view.setIsMainUserId(true);
+ }
+ mEditors.addView(view);
+ }
+
+ this.updateEditorsVisible();
+ }
+
+ public void setKeys(Vector<PGPSecretKey> list, Vector<Integer> usages) {
+ if (mType != Id.type.key) {
+ return;
+ }
+
+ mEditors.removeAllViews();
+
+ // go through all keys and set view based on them
+ for (int i = 0; i < list.size(); i++) {
+ KeyEditor view = (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item, mEditors,
+ false);
+ view.setEditorListener(this);
+ boolean isMasterKey = (mEditors.getChildCount() == 0);
+ view.setValue(list.get(i), isMasterKey, usages.get(i));
+ mEditors.addView(view);
+ }
+
+ this.updateEditorsVisible();
+ }
+
+ private void createKey() {
+ // Send all information needed to service to edit key in other thread
+ Intent intent = new Intent(mActivity, KeychainIntentService.class);
+
+ intent.putExtra(KeychainIntentService.EXTRA_ACTION, KeychainIntentService.ACTION_GENERATE_KEY);
+
+ // fill values for this action
+ Bundle data = new Bundle();
+
+ String passPhrase;
+ if (mEditors.getChildCount() > 0) {
+ PGPSecretKey masterKey = ((KeyEditor) mEditors.getChildAt(0)).getValue();
+ passPhrase = PassphraseCacheService
+ .getCachedPassphrase(mActivity, masterKey.getKeyID());
+
+ data.putByteArray(KeychainIntentService.GENERATE_KEY_MASTER_KEY,
+ PgpConversionHelper.PGPSecretKeyToBytes(masterKey));
+ } else {
+ passPhrase = "";
+ }
+ data.putString(KeychainIntentService.GENERATE_KEY_SYMMETRIC_PASSPHRASE, passPhrase);
+ data.putInt(KeychainIntentService.GENERATE_KEY_ALGORITHM, mNewKeyAlgorithmChoice.getId());
+ data.putInt(KeychainIntentService.GENERATE_KEY_KEY_SIZE, mNewKeySize);
+
+ intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
+
+ // show progress dialog
+ mGeneratingDialog = ProgressDialogFragment.newInstance(R.string.progress_generating,
+ ProgressDialog.STYLE_SPINNER);
+
+ // Message is received after generating is done in ApgService
+ KeychainIntentServiceHandler saveHandler = new KeychainIntentServiceHandler(mActivity, mGeneratingDialog) {
+ public void handleMessage(Message message) {
+ // handle messages by standard ApgHandler first
+ super.handleMessage(message);
+
+ if (message.arg1 == KeychainIntentServiceHandler.MESSAGE_OKAY) {
+ // get new key from data bundle returned from service
+ Bundle data = message.getData();
+ PGPSecretKeyRing newKeyRing = (PGPSecretKeyRing) PgpConversionHelper
+ .BytesToPGPKeyRing(data.getByteArray(KeychainIntentService.RESULT_NEW_KEY));
+
+ boolean isMasterKey = (mEditors.getChildCount() == 0);
+
+ // take only the key from this ring
+ PGPSecretKey newKey = null;
+ @SuppressWarnings("unchecked")
+ Iterator<PGPSecretKey> it = newKeyRing.getSecretKeys();
+
+ if (isMasterKey) {
+ newKey = it.next();
+ } else {
+ // first one is the master key
+ it.next();
+ newKey = it.next();
+ }
+
+ // add view with new key
+ KeyEditor view = (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item,
+ mEditors, false);
+ view.setEditorListener(SectionView.this);
+ view.setValue(newKey, isMasterKey, -1);
+ mEditors.addView(view);
+ SectionView.this.updateEditorsVisible();
+ }
+ };
+ };
+
+ // Create a new Messenger for the communication back
+ Messenger messenger = new Messenger(saveHandler);
+ intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
+
+ mGeneratingDialog.show(mActivity.getSupportFragmentManager(), "dialog");
+
+ // start service with intent
+ mActivity.startService(intent);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SelectKeyCursorAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SelectKeyCursorAdapter.java
new file mode 100644
index 000000000..da8ac94ee
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SelectKeyCursorAdapter.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import org.sufficientlysecure.keychain.Id;
+import org.sufficientlysecure.keychain.helper.OtherHelper;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
+import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds;
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v4.widget.CursorAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class SelectKeyCursorAdapter extends CursorAdapter {
+
+ protected int mKeyType;
+
+ private LayoutInflater mInflater;
+ private ListView mListView;
+
+ public final static String PROJECTION_ROW_AVAILABLE = "available";
+ public final static String PROJECTION_ROW_VALID = "valid";
+
+ @SuppressWarnings("deprecation")
+ public SelectKeyCursorAdapter(Context context, ListView listView, Cursor c, int keyType) {
+ super(context, c);
+
+ mInflater = LayoutInflater.from(context);
+ mListView = listView;
+ mKeyType = keyType;
+ }
+
+ public String getUserId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getString(mCursor.getColumnIndex(UserIds.USER_ID));
+ }
+
+ public long getMasterKeyId(int position) {
+ mCursor.moveToPosition(position);
+ return mCursor.getLong(mCursor.getColumnIndex(KeyRings.MASTER_KEY_ID));
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ boolean valid = cursor.getInt(cursor
+ .getColumnIndex(PROJECTION_ROW_VALID)) > 0;
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId);
+ mainUserId.setText(R.string.unknownUserId);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest);
+ mainUserIdRest.setText("");
+ TextView keyId = (TextView) view.findViewById(R.id.keyId);
+ keyId.setText(R.string.noKey);
+ TextView status = (TextView) view.findViewById(R.id.status);
+ status.setText(R.string.unknownStatus);
+
+ String userId = cursor.getString(cursor.getColumnIndex(UserIds.USER_ID));
+ if (userId != null) {
+ String[] userIdSplit = OtherHelper.splitUserId(userId);
+
+ if (userIdSplit[1] != null) {
+ mainUserIdRest.setText(userIdSplit[1]);
+ }
+ mainUserId.setText(userIdSplit[0]);
+ }
+
+ long masterKeyId = cursor.getLong(cursor.getColumnIndex(KeyRings.MASTER_KEY_ID));
+ keyId.setText(PgpHelper.getSmallFingerPrint(masterKeyId));
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ if (valid) {
+ if (mKeyType == Id.type.public_key) {
+ status.setText(R.string.canEncrypt);
+ } else {
+ status.setText(R.string.canSign);
+ }
+ } else {
+ if (cursor.getInt(cursor
+ .getColumnIndex(PROJECTION_ROW_AVAILABLE)) > 0) {
+ // has some CAN_ENCRYPT keys, but col(ROW_VALID) = 0, so must be revoked or
+ // expired
+ status.setText(R.string.expired);
+ } else {
+ status.setText(R.string.noKey);
+ }
+ }
+
+ CheckBox selected = (CheckBox) view.findViewById(R.id.selected);
+ if (mKeyType == Id.type.public_key) {
+ selected.setVisibility(View.VISIBLE);
+
+ if (!valid) {
+ mListView.setItemChecked(cursor.getPosition(), false);
+ }
+
+ selected.setChecked(mListView.isItemChecked(cursor.getPosition()));
+ selected.setEnabled(valid);
+ } else {
+ selected.setVisibility(View.GONE);
+ }
+
+ status.setText(status.getText() + " ");
+
+ view.setEnabled(valid);
+ mainUserId.setEnabled(valid);
+ mainUserIdRest.setEnabled(valid);
+ keyId.setEnabled(valid);
+ status.setEnabled(valid);
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return mInflater.inflate(R.layout.select_key_item, null);
+ }
+
+} \ No newline at end of file
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java
new file mode 100644
index 000000000..2215d29e7
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/UserIdEditor.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.ui.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.sufficientlysecure.keychain.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+
+public class UserIdEditor extends LinearLayout implements Editor, OnClickListener {
+ private EditorListener mEditorListener = null;
+
+ private ImageButton mDeleteButton;
+ private RadioButton mIsMainUserId;
+ private EditText mName;
+ private EditText mEmail;
+ private EditText mComment;
+
+ // see http://www.regular-expressions.info/email.html
+ // RFC 2822 if we omit the syntax using double quotes and square brackets
+ // android.util.Patterns.EMAIL_ADDRESS is only available as of Android 2.2+
+ private static final Pattern EMAIL_PATTERN = Pattern
+ .compile(
+ "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?",
+ Pattern.CASE_INSENSITIVE);
+
+ public static class NoNameException extends Exception {
+ static final long serialVersionUID = 0xf812773343L;
+
+ public NoNameException(String message) {
+ super(message);
+ }
+ }
+
+ public static class NoEmailException extends Exception {
+ static final long serialVersionUID = 0xf812773344L;
+
+ public NoEmailException(String message) {
+ super(message);
+ }
+ }
+
+ public static class InvalidEmailException extends Exception {
+ static final long serialVersionUID = 0xf812773345L;
+
+ public InvalidEmailException(String message) {
+ super(message);
+ }
+ }
+
+ public UserIdEditor(Context context) {
+ super(context);
+ }
+
+ public UserIdEditor(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ setDrawingCacheEnabled(true);
+ setAlwaysDrawnWithCacheEnabled(true);
+
+ mDeleteButton = (ImageButton) findViewById(R.id.delete);
+ mDeleteButton.setOnClickListener(this);
+ mIsMainUserId = (RadioButton) findViewById(R.id.isMainUserId);
+ mIsMainUserId.setOnClickListener(this);
+
+ mName = (EditText) findViewById(R.id.name);
+ mEmail = (EditText) findViewById(R.id.email);
+ mComment = (EditText) findViewById(R.id.comment);
+
+ super.onFinishInflate();
+ }
+
+ public void setValue(String userId) {
+ mName.setText("");
+ mComment.setText("");
+ mEmail.setText("");
+
+ Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$");
+ Matcher matcher = withComment.matcher(userId);
+ if (matcher.matches()) {
+ mName.setText(matcher.group(1));
+ mComment.setText(matcher.group(2));
+ mEmail.setText(matcher.group(3));
+ return;
+ }
+
+ Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$");
+ matcher = withoutComment.matcher(userId);
+ if (matcher.matches()) {
+ mName.setText(matcher.group(1));
+ mEmail.setText(matcher.group(2));
+ return;
+ }
+ }
+
+ public String getValue() throws NoNameException, NoEmailException, InvalidEmailException {
+ String name = ("" + mName.getText()).trim();
+ String email = ("" + mEmail.getText()).trim();
+ String comment = ("" + mComment.getText()).trim();
+
+ if (email.length() > 0) {
+ Matcher emailMatcher = EMAIL_PATTERN.matcher(email);
+ if (!emailMatcher.matches()) {
+ throw new InvalidEmailException(getContext().getString(R.string.error_invalidEmail,
+ email));
+ }
+ }
+
+ String userId = name;
+ if (comment.length() > 0) {
+ userId += " (" + comment + ")";
+ }
+ if (email.length() > 0) {
+ userId += " <" + email + ">";
+ }
+
+ if (userId.equals("")) {
+ // ok, empty one...
+ return userId;
+ }
+
+ // otherwise make sure that name and email exist
+ if (name.equals("")) {
+ throw new NoNameException("need a name");
+ }
+
+ if (email.equals("")) {
+ throw new NoEmailException("need an email");
+ }
+
+ return userId;
+ }
+
+ public void onClick(View v) {
+ final ViewGroup parent = (ViewGroup) getParent();
+ if (v == mDeleteButton) {
+ boolean wasMainUserId = mIsMainUserId.isChecked();
+ parent.removeView(this);
+ if (mEditorListener != null) {
+ mEditorListener.onDeleted(this);
+ }
+ if (wasMainUserId && parent.getChildCount() > 0) {
+ UserIdEditor editor = (UserIdEditor) parent.getChildAt(0);
+ editor.setIsMainUserId(true);
+ }
+ } else if (v == mIsMainUserId) {
+ for (int i = 0; i < parent.getChildCount(); ++i) {
+ UserIdEditor editor = (UserIdEditor) parent.getChildAt(i);
+ if (editor == this) {
+ editor.setIsMainUserId(true);
+ } else {
+ editor.setIsMainUserId(false);
+ }
+ }
+ }
+ }
+
+ public void setIsMainUserId(boolean value) {
+ mIsMainUserId.setChecked(value);
+ }
+
+ public boolean isMainUserId() {
+ return mIsMainUserId.isChecked();
+ }
+
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Choice.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Choice.java
new file mode 100644
index 000000000..9e0042c00
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Choice.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+public class Choice {
+ private String mName;
+ private int mId;
+
+ public Choice() {
+ mId = -1;
+ mName = "";
+ }
+
+ public Choice(int id, String name) {
+ mId = id;
+ mName = name;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public String toString() {
+ return mName;
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/HkpKeyServer.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/HkpKeyServer.java
new file mode 100644
index 000000000..586a03498
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/HkpKeyServer.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2011 Senecaso
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.sufficientlysecure.keychain.helper.PgpHelper;
+import org.sufficientlysecure.keychain.helper.PgpMain;
+
+import android.text.Html;
+
+public class HkpKeyServer extends KeyServer {
+ private static class HttpError extends Exception {
+ private static final long serialVersionUID = 1718783705229428893L;
+ private int mCode;
+ private String mData;
+
+ public HttpError(int code, String data) {
+ super("" + code + ": " + data);
+ mCode = code;
+ mData = data;
+ }
+
+ public int getCode() {
+ return mCode;
+ }
+
+ public String getData() {
+ return mData;
+ }
+ }
+
+ private String mHost;
+ private short mPort = 11371;
+
+ // example:
+ // pub 2048R/<a href="/pks/lookup?op=get&search=0x887DF4BE9F5C9090">9F5C9090</a> 2009-08-17 <a
+ // href="/pks/lookup?op=vindex&search=0x887DF4BE9F5C9090">Jörg Runge
+ // &lt;joerg@joergrunge.de&gt;</a>
+ public static Pattern PUB_KEY_LINE = Pattern
+ .compile(
+ "pub +([0-9]+)([a-z]+)/.*?0x([0-9a-z]+).*? +([0-9-]+) +(.+)[\n\r]+((?: +.+[\n\r]+)*)",
+ Pattern.CASE_INSENSITIVE);
+ public static Pattern USER_ID_LINE = Pattern.compile("^ +(.+)$", Pattern.MULTILINE
+ | Pattern.CASE_INSENSITIVE);
+
+ public HkpKeyServer(String host) {
+ mHost = host;
+ }
+
+ public HkpKeyServer(String host, short port) {
+ mHost = host;
+ mPort = port;
+ }
+
+ static private String readAll(InputStream in, String encoding) throws IOException {
+ ByteArrayOutputStream raw = new ByteArrayOutputStream();
+
+ byte buffer[] = new byte[1 << 16];
+ int n = 0;
+ while ((n = in.read(buffer)) != -1) {
+ raw.write(buffer, 0, n);
+ }
+
+ if (encoding == null) {
+ encoding = "utf8";
+ }
+ return raw.toString(encoding);
+ }
+
+ private String query(String request) throws QueryException, HttpError {
+ InetAddress ips[];
+ try {
+ ips = InetAddress.getAllByName(mHost);
+ } catch (UnknownHostException e) {
+ throw new QueryException(e.toString());
+ }
+ for (int i = 0; i < ips.length; ++i) {
+ try {
+ String url = "http://" + ips[i].getHostAddress() + ":" + mPort + request;
+ URL realUrl = new URL(url);
+ HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
+ conn.setConnectTimeout(5000);
+ conn.setReadTimeout(25000);
+ conn.connect();
+ int response = conn.getResponseCode();
+ if (response >= 200 && response < 300) {
+ return readAll(conn.getInputStream(), conn.getContentEncoding());
+ } else {
+ String data = readAll(conn.getErrorStream(), conn.getContentEncoding());
+ throw new HttpError(response, data);
+ }
+ } catch (MalformedURLException e) {
+ // nothing to do, try next IP
+ } catch (IOException e) {
+ // nothing to do, try next IP
+ }
+ }
+
+ throw new QueryException("querying server(s) for '" + mHost + "' failed");
+ }
+
+ @Override
+ public ArrayList<KeyInfo> search(String query) throws QueryException, TooManyResponses,
+ InsufficientQuery {
+ ArrayList<KeyInfo> results = new ArrayList<KeyInfo>();
+
+ if (query.length() < 3) {
+ throw new InsufficientQuery();
+ }
+
+ String encodedQuery;
+ try {
+ encodedQuery = URLEncoder.encode(query, "utf8");
+ } catch (UnsupportedEncodingException e) {
+ return null;
+ }
+ String request = "/pks/lookup?op=index&search=" + encodedQuery;
+
+ String data = null;
+ try {
+ data = query(request);
+ } catch (HttpError e) {
+ if (e.getCode() == 404) {
+ return results;
+ } else {
+ if (e.getData().toLowerCase().contains("no keys found")) {
+ return results;
+ } else if (e.getData().toLowerCase().contains("too many")) {
+ throw new TooManyResponses();
+ } else if (e.getData().toLowerCase().contains("insufficient")) {
+ throw new InsufficientQuery();
+ }
+ }
+ throw new QueryException("querying server(s) for '" + mHost + "' failed");
+ }
+
+ Matcher matcher = PUB_KEY_LINE.matcher(data);
+ while (matcher.find()) {
+ KeyInfo info = new KeyInfo();
+ info.size = Integer.parseInt(matcher.group(1));
+ info.algorithm = matcher.group(2);
+ info.keyId = PgpHelper.keyFromHex(matcher.group(3));
+ info.fingerPrint = PgpHelper.getSmallFingerPrint(info.keyId);
+ String chunks[] = matcher.group(4).split("-");
+ info.date = new GregorianCalendar(Integer.parseInt(chunks[0]),
+ Integer.parseInt(chunks[1]), Integer.parseInt(chunks[2])).getTime();
+ info.userIds = new ArrayList<String>();
+ if (matcher.group(5).startsWith("*** KEY")) {
+ info.revoked = matcher.group(5);
+ } else {
+ String tmp = matcher.group(5).replaceAll("<.*?>", "");
+ tmp = Html.fromHtml(tmp).toString();
+ info.userIds.add(tmp);
+ }
+ if (matcher.group(6).length() > 0) {
+ Matcher matcher2 = USER_ID_LINE.matcher(matcher.group(6));
+ while (matcher2.find()) {
+ String tmp = matcher2.group(1).replaceAll("<.*?>", "");
+ tmp = Html.fromHtml(tmp).toString();
+ info.userIds.add(tmp);
+ }
+ }
+ results.add(info);
+ }
+
+ return results;
+ }
+
+ @Override
+ public String get(long keyId) throws QueryException {
+ HttpClient client = new DefaultHttpClient();
+ try {
+ HttpGet get = new HttpGet("http://" + mHost + ":" + mPort
+ + "/pks/lookup?op=get&search=0x" + PgpHelper.keyToHex(keyId));
+
+ HttpResponse response = client.execute(get);
+ if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
+ throw new QueryException("not found");
+ }
+
+ HttpEntity entity = response.getEntity();
+ InputStream is = entity.getContent();
+ String data = readAll(is, EntityUtils.getContentCharSet(entity));
+ Matcher matcher = PgpMain.PGP_PUBLIC_KEY.matcher(data);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ } catch (IOException e) {
+ // nothing to do, better luck on the next keyserver
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+
+ return null;
+ }
+
+ @Override
+ public void add(String armouredText) throws AddKeyException {
+ HttpClient client = new DefaultHttpClient();
+ try {
+ HttpPost post = new HttpPost("http://" + mHost + ":" + mPort + "/pks/add");
+
+ List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
+ nameValuePairs.add(new BasicNameValuePair("keytext", armouredText));
+ post.setEntity(new UrlEncodedFormEntity(nameValuePairs));
+
+ HttpResponse response = client.execute(post);
+ if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
+ throw new AddKeyException();
+ }
+ } catch (IOException e) {
+ // nothing to do, better luck on the next keyserver
+ } finally {
+ client.getConnectionManager().shutdown();
+ }
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/InputData.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/InputData.java
new file mode 100644
index 000000000..f5bcbf88a
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/InputData.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import java.io.InputStream;
+
+
+public class InputData {
+ private PositionAwareInputStream mInputStream;
+ private long mSize;
+
+ public InputData(InputStream inputStream, long size) {
+ mInputStream = new PositionAwareInputStream(inputStream);
+ mSize = size;
+ }
+
+ public InputStream getInputStream() {
+ return mInputStream;
+ }
+
+ public long getSize() {
+ return mSize;
+ }
+
+ public long getStreamPosition() {
+ return mInputStream.position();
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/IterableIterator.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/IterableIterator.java
new file mode 100644
index 000000000..caaa07524
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/IterableIterator.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import java.util.Iterator;
+
+public class IterableIterator<T> implements Iterable<T> {
+ private Iterator<T> mIter;
+
+ public IterableIterator(Iterator<T> iter) {
+ mIter = iter;
+ }
+
+ public Iterator<T> iterator() {
+ return mIter;
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/KeyServer.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/KeyServer.java
new file mode 100644
index 000000000..cdc188887
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/KeyServer.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2011 Senecaso
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public abstract class KeyServer {
+ static public class QueryException extends Exception {
+ private static final long serialVersionUID = 2703768928624654512L;
+
+ public QueryException(String message) {
+ super(message);
+ }
+ }
+
+ static public class TooManyResponses extends Exception {
+ private static final long serialVersionUID = 2703768928624654513L;
+ }
+
+ static public class InsufficientQuery extends Exception {
+ private static final long serialVersionUID = 2703768928624654514L;
+ }
+
+ static public class AddKeyException extends Exception {
+ private static final long serialVersionUID = -507574859137295530L;
+ }
+
+ static public class KeyInfo implements Serializable, Parcelable {
+ private static final long serialVersionUID = -7797972113284992662L;
+ public ArrayList<String> userIds;
+ public String revoked;
+ public Date date;
+ public String fingerPrint;
+ public long keyId;
+ public int size;
+ public String algorithm;
+
+ public KeyInfo() {
+ userIds = new ArrayList<String>();
+ }
+
+ public KeyInfo(Parcel in) {
+ this();
+
+ in.readStringList(this.userIds);
+ this.revoked = in.readString();
+ this.date = (Date) in.readSerializable();
+ this.fingerPrint = in.readString();
+ this.keyId = in.readLong();
+ this.size = in.readInt();
+ this.algorithm = in.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStringList(userIds);
+ dest.writeString(revoked);
+ dest.writeSerializable(date);
+ dest.writeString(fingerPrint);
+ dest.writeLong(keyId);
+ dest.writeInt(size);
+ dest.writeString(algorithm);
+ }
+ }
+
+ abstract List<KeyInfo> search(String query) throws QueryException, TooManyResponses,
+ InsufficientQuery;
+
+ abstract String get(long keyId) throws QueryException;
+
+ abstract void add(String armouredText) throws AddKeyException;
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Log.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Log.java
new file mode 100644
index 000000000..cad01d708
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Log.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import org.sufficientlysecure.keychain.Constants;
+
+/**
+ * Wraps Android Logging to enable or disable debug output using Constants
+ *
+ */
+public final class Log {
+
+ public static void v(String tag, String msg) {
+ if (Constants.DEBUG) {
+ android.util.Log.v(tag, msg);
+ }
+ }
+
+ public static void v(String tag, String msg, Throwable tr) {
+ if (Constants.DEBUG) {
+ android.util.Log.v(tag, msg, tr);
+ }
+ }
+
+ public static void d(String tag, String msg) {
+ if (Constants.DEBUG) {
+ android.util.Log.d(tag, msg);
+ }
+ }
+
+ public static void d(String tag, String msg, Throwable tr) {
+ if (Constants.DEBUG) {
+ android.util.Log.d(tag, msg, tr);
+ }
+ }
+
+ public static void i(String tag, String msg) {
+ if (Constants.DEBUG) {
+ android.util.Log.i(tag, msg);
+ }
+ }
+
+ public static void i(String tag, String msg, Throwable tr) {
+ if (Constants.DEBUG) {
+ android.util.Log.i(tag, msg, tr);
+ }
+ }
+
+ public static void w(String tag, String msg) {
+ android.util.Log.w(tag, msg);
+ }
+
+ public static void w(String tag, String msg, Throwable tr) {
+ android.util.Log.w(tag, msg, tr);
+ }
+
+ public static void w(String tag, Throwable tr) {
+ android.util.Log.w(tag, tr);
+ }
+
+ public static void e(String tag, String msg) {
+ android.util.Log.e(tag, msg);
+ }
+
+ public static void e(String tag, String msg, Throwable tr) {
+ android.util.Log.e(tag, msg, tr);
+ }
+
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/PositionAwareInputStream.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/PositionAwareInputStream.java
new file mode 100644
index 000000000..a783d7820
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/PositionAwareInputStream.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class PositionAwareInputStream extends InputStream {
+ private InputStream mStream;
+ private long mPosition;
+
+ public PositionAwareInputStream(InputStream in) {
+ mStream = in;
+ mPosition = 0;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int ch = mStream.read();
+ ++mPosition;
+ return ch;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return mStream.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ mStream.close();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ int result = mStream.read(b);
+ mPosition += result;
+ return result;
+ }
+
+ @Override
+ public int read(byte[] b, int offset, int length) throws IOException {
+ int result = mStream.read(b, offset, length);
+ mPosition += result;
+ return result;
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ mStream.reset();
+ mPosition = 0;
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ long result = mStream.skip(n);
+ mPosition += result;
+ return result;
+ }
+
+ public long position() {
+ return mPosition;
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Primes.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Primes.java
new file mode 100644
index 000000000..f503227a3
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/Primes.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+import java.math.BigInteger;
+
+/**
+ * Primes for ElGamal
+ */
+public final class Primes {
+ // taken from http://www.ietf.org/rfc/rfc3526.txt
+ public static final String P1536 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA237327 FFFFFFFF FFFFFFFF";
+
+ public static final String P2048 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" +
+ "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" +
+ "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" +
+ "15728E5A 8AACAA68 FFFFFFFF FFFFFFFF";
+
+ public static final String P3072 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" +
+ "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" +
+ "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" +
+ "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" +
+ "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" +
+ "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" +
+ "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" +
+ "43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF";
+
+ public static final String P4096 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" +
+ "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" +
+ "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" +
+ "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" +
+ "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" +
+ "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" +
+ "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" +
+ "43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7" +
+ "88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA" +
+ "2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6" +
+ "287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED" +
+ "1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9" +
+ "93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199" +
+ "FFFFFFFF FFFFFFFF";
+
+ public static final String P6144 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" +
+ "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" +
+ "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" +
+ "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" +
+ "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" +
+ "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" +
+ "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" +
+ "43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7" +
+ "88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA" +
+ "2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6" +
+ "287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED" +
+ "1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9" +
+ "93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" +
+ "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD" +
+ "F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831" +
+ "179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B" +
+ "DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF" +
+ "5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6" +
+ "D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3" +
+ "23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" +
+ "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328" +
+ "06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C" +
+ "DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE" +
+ "12BF2D5B 0B7474D6 E694F91E 6DCC4024 FFFFFFFF FFFFFFFF";
+
+ public static final String P8192 =
+ "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1" +
+ "29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD" +
+ "EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245" +
+ "E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED" +
+ "EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D" +
+ "C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F" +
+ "83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+ "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B" +
+ "E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9" +
+ "DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510" +
+ "15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64" +
+ "ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7" +
+ "ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B" +
+ "F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+ "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31" +
+ "43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7" +
+ "88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA" +
+ "2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6" +
+ "287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED" +
+ "1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9" +
+ "93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" +
+ "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD" +
+ "F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831" +
+ "179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B" +
+ "DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF" +
+ "5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6" +
+ "D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3" +
+ "23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" +
+ "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328" +
+ "06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C" +
+ "DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE" +
+ "12BF2D5B 0B7474D6 E694F91E 6DBE1159 74A3926F 12FEE5E4" +
+ "38777CB6 A932DF8C D8BEC4D0 73B931BA 3BC832B6 8D9DD300" +
+ "741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 5AE4F568" +
+ "3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9" +
+ "22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B" +
+ "4BCBC886 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A" +
+ "062B3CF5 B3A278A6 6D2A13F8 3F44F82D DF310EE0 74AB6A36" +
+ "4597E899 A0255DC1 64F31CC5 0846851D F9AB4819 5DED7EA1" +
+ "B1D510BD 7EE74D73 FAF36BC3 1ECFA268 359046F4 EB879F92" +
+ "4009438B 481C6CD7 889A002E D5EE382B C9190DA6 FC026E47" +
+ "9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71" +
+ "60C980DD 98EDD3DF FFFFFFFF FFFFFFFF";
+
+ public static BigInteger getBestPrime(int keySize) {
+ String primeString;
+ if (keySize >= (8192 + 6144) / 2) {
+ primeString = P8192;
+ } else if (keySize >= (6144 + 4096) / 2) {
+ primeString = P6144;
+ } else if (keySize >= (4096 + 3072) / 2) {
+ primeString = P4096;
+ } else if (keySize >= (3072 + 2048) / 2) {
+ primeString = P3072;
+ } else if (keySize >= (2048 + 1536) / 2) {
+ primeString = P2048;
+ } else {
+ primeString = P1536;
+ }
+
+ return new BigInteger(primeString.replaceAll(" ", ""), 16);
+ }
+}
diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/ProgressDialogUpdater.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/ProgressDialogUpdater.java
new file mode 100644
index 000000000..26c05ad0a
--- /dev/null
+++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/ProgressDialogUpdater.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 Thialfihar <thi@thialfihar.org>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.sufficientlysecure.keychain.util;
+
+public interface ProgressDialogUpdater {
+ void setProgress(String message, int current, int total);
+
+ void setProgress(int resourceId, int current, int total);
+
+ void setProgress(int current, int total);
+}