aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThialfihar <thialfihar@gmail.com>2010-04-06 19:54:51 +0000
committerThialfihar <thialfihar@gmail.com>2010-04-06 19:54:51 +0000
commit42f1720bb32b5404ae9b78c0b042b143b6f507af (patch)
tree9fc2abf69b8e8d0a7268a044ca6dfa301bd4f252
parentaf9342a2cc06b070f1537f69d11d4f1d5a4578ce (diff)
downloadopen-keychain-42f1720bb32b5404ae9b78c0b042b143b6f507af.tar.gz
open-keychain-42f1720bb32b5404ae9b78c0b042b143b6f507af.tar.bz2
open-keychain-42f1720bb32b5404ae9b78c0b042b143b6f507af.zip
initial commit of v0.8.0
-rw-r--r--AndroidManifest.xml89
-rw-r--r--default.properties13
-rw-r--r--res/drawable-finger/btn_circle.xml32
-rw-r--r--res/drawable-hdpi-finger/btn_circle_disable.pngbin0 -> 2631 bytes
-rw-r--r--res/drawable-hdpi-finger/btn_circle_disable_focused.pngbin0 -> 3001 bytes
-rw-r--r--res/drawable-hdpi-finger/btn_circle_normal.pngbin0 -> 1974 bytes
-rw-r--r--res/drawable-hdpi-finger/btn_circle_pressed.pngbin0 -> 2624 bytes
-rw-r--r--res/drawable-hdpi-finger/btn_circle_selected.pngbin0 -> 2554 bytes
-rw-r--r--res/drawable-hdpi-finger/ic_btn_round_minus.pngbin0 -> 536 bytes
-rw-r--r--res/drawable-hdpi-finger/ic_btn_round_plus.pngbin0 -> 1316 bytes
-rw-r--r--res/drawable-hdpi/encrypted.pngbin0 -> 3561 bytes
-rw-r--r--res/drawable-hdpi/encrypted_large.pngbin0 -> 5244 bytes
-rw-r--r--res/drawable-hdpi/encrypted_small.pngbin0 -> 2187 bytes
-rw-r--r--res/drawable-hdpi/icon.pngbin0 -> 5563 bytes
-rw-r--r--res/drawable-hdpi/key.pngbin0 -> 3675 bytes
-rw-r--r--res/drawable-hdpi/key_large.pngbin0 -> 5550 bytes
-rw-r--r--res/drawable-hdpi/key_small.pngbin0 -> 2088 bytes
-rw-r--r--res/drawable-hdpi/overlay_error.pngbin0 -> 1986 bytes
-rw-r--r--res/drawable-hdpi/overlay_ok.pngbin0 -> 1702 bytes
-rw-r--r--res/drawable-hdpi/signed.pngbin0 -> 3858 bytes
-rw-r--r--res/drawable-hdpi/signed_large.pngbin0 -> 5928 bytes
-rw-r--r--res/drawable-hdpi/signed_small.pngbin0 -> 2219 bytes
-rw-r--r--res/drawable-ldpi/encrypted.pngbin0 -> 1513 bytes
-rw-r--r--res/drawable-ldpi/encrypted_large.pngbin0 -> 2486 bytes
-rw-r--r--res/drawable-ldpi/encrypted_small.pngbin0 -> 1176 bytes
-rw-r--r--res/drawable-ldpi/icon.pngbin0 -> 1948 bytes
-rw-r--r--res/drawable-ldpi/key.pngbin0 -> 1484 bytes
-rw-r--r--res/drawable-ldpi/key_large.pngbin0 -> 2462 bytes
-rw-r--r--res/drawable-ldpi/key_small.pngbin0 -> 1074 bytes
-rw-r--r--res/drawable-ldpi/overlay_error.pngbin0 -> 1192 bytes
-rw-r--r--res/drawable-ldpi/overlay_ok.pngbin0 -> 1038 bytes
-rw-r--r--res/drawable-ldpi/signed.pngbin0 -> 1576 bytes
-rw-r--r--res/drawable-ldpi/signed_large.pngbin0 -> 2611 bytes
-rw-r--r--res/drawable-ldpi/signed_small.pngbin0 -> 1149 bytes
-rw-r--r--res/drawable-mdpi-finger/btn_circle_disable.pngbin0 -> 938 bytes
-rw-r--r--res/drawable-mdpi-finger/btn_circle_disable_focused.pngbin0 -> 1436 bytes
-rw-r--r--res/drawable-mdpi-finger/btn_circle_normal.pngbin0 -> 1249 bytes
-rw-r--r--res/drawable-mdpi-finger/btn_circle_pressed.pngbin0 -> 1613 bytes
-rw-r--r--res/drawable-mdpi-finger/btn_circle_selected.pngbin0 -> 1645 bytes
-rw-r--r--res/drawable-mdpi-finger/ic_btn_round_minus.pngbin0 -> 288 bytes
-rw-r--r--res/drawable-mdpi-finger/ic_btn_round_plus.pngbin0 -> 526 bytes
-rw-r--r--res/drawable-mdpi/encrypted.pngbin0 -> 2486 bytes
-rw-r--r--res/drawable-mdpi/encrypted_large.pngbin0 -> 3561 bytes
-rw-r--r--res/drawable-mdpi/encrypted_small.pngbin0 -> 1513 bytes
-rw-r--r--res/drawable-mdpi/icon.pngbin0 -> 2947 bytes
-rw-r--r--res/drawable-mdpi/key.pngbin0 -> 2462 bytes
-rw-r--r--res/drawable-mdpi/key_large.pngbin0 -> 3675 bytes
-rw-r--r--res/drawable-mdpi/key_small.pngbin0 -> 1484 bytes
-rw-r--r--res/drawable-mdpi/overlay_error.pngbin0 -> 1539 bytes
-rw-r--r--res/drawable-mdpi/overlay_ok.pngbin0 -> 1305 bytes
-rw-r--r--res/drawable-mdpi/signed.pngbin0 -> 2611 bytes
-rw-r--r--res/drawable-mdpi/signed_large.pngbin0 -> 3858 bytes
-rw-r--r--res/drawable-mdpi/signed_small.pngbin0 -> 1576 bytes
-rw-r--r--res/layout/account_item.xml35
-rw-r--r--res/layout/create_key.xml52
-rw-r--r--res/layout/decrypt_message.xml97
-rw-r--r--res/layout/edit_key.xml58
-rw-r--r--res/layout/edit_key_key_item.xml113
-rw-r--r--res/layout/edit_key_section.xml66
-rw-r--r--res/layout/edit_key_user_id_item.xml96
-rw-r--r--res/layout/encrypt_message.xml95
-rw-r--r--res/layout/key_list_child_item_master_key.xml72
-rw-r--r--res/layout/key_list_child_item_sub_key.xml70
-rw-r--r--res/layout/key_list_child_item_user_id.xml33
-rw-r--r--res/layout/key_list_group_item.xml52
-rw-r--r--res/layout/mailbox_message_item.xml57
-rw-r--r--res/layout/main.xml61
-rw-r--r--res/layout/select_public_key.xml54
-rw-r--r--res/layout/select_public_key_item.xml96
-rw-r--r--res/layout/select_secret_key.xml30
-rw-r--r--res/layout/select_secret_key_item.xml88
-rw-r--r--res/values/strings.xml68
-rw-r--r--res/values/styles.xml27
-rw-r--r--src/org/thialfihar/android/apg/Apg.java1496
-rw-r--r--src/org/thialfihar/android/apg/AskForSecretKeyPassPhrase.java118
-rw-r--r--src/org/thialfihar/android/apg/DecryptMessageActivity.java343
-rw-r--r--src/org/thialfihar/android/apg/EditKeyActivity.java401
-rw-r--r--src/org/thialfihar/android/apg/EncryptMessageActivity.java428
-rw-r--r--src/org/thialfihar/android/apg/MailListActivity.java202
-rw-r--r--src/org/thialfihar/android/apg/MainActivity.java394
-rw-r--r--src/org/thialfihar/android/apg/ProgressDialogUpdater.java22
-rw-r--r--src/org/thialfihar/android/apg/PublicKeyListActivity.java660
-rw-r--r--src/org/thialfihar/android/apg/SecretKeyListActivity.java758
-rw-r--r--src/org/thialfihar/android/apg/SelectPublicKeyListActivity.java259
-rw-r--r--src/org/thialfihar/android/apg/SelectSecretKeyListActivity.java209
-rw-r--r--src/org/thialfihar/android/apg/provider/Accounts.java22
-rw-r--r--src/org/thialfihar/android/apg/provider/Accounts1.java36
-rw-r--r--src/org/thialfihar/android/apg/provider/DataProvider.java494
-rw-r--r--src/org/thialfihar/android/apg/provider/PublicKeys.java22
-rw-r--r--src/org/thialfihar/android/apg/provider/PublicKeys1.java42
-rw-r--r--src/org/thialfihar/android/apg/provider/SecretKeys.java22
-rw-r--r--src/org/thialfihar/android/apg/provider/SecretKeys1.java42
-rw-r--r--src/org/thialfihar/android/apg/ui/widget/Editor.java25
-rw-r--r--src/org/thialfihar/android/apg/ui/widget/KeyEditor.java248
-rw-r--r--src/org/thialfihar/android/apg/ui/widget/SectionView.java323
-rw-r--r--src/org/thialfihar/android/apg/ui/widget/UserIdEditor.java193
-rw-r--r--src/org/thialfihar/android/apg/utils/Choice.java44
-rw-r--r--src/org/thialfihar/android/apg/utils/IterableIterator.java31
98 files changed, 8288 insertions, 0 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 000000000..5a41f262d
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.thialfihar.android.apg"
+ android:versionCode="3" android:versionName="0.8.0">
+ <application android:icon="@drawable/icon" android:label="@string/app_name">
+ <activity android:name=".MainActivity"
+ android:label="@string/app_name"
+ android:configChanges="keyboardHidden|orientation|keyboard">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".PublicKeyListActivity"
+ android:label="@string/title_managePublicKeys"
+ android:configChanges="keyboardHidden|orientation|keyboard">
+ </activity>
+
+ <activity android:name=".SecretKeyListActivity"
+ android:label="@string/title_manageSecretKeys"
+ android:configChanges="keyboardHidden|orientation|keyboard">
+ </activity>
+
+ <activity android:name=".EditKeyActivity"
+ android:label="@string/title_editKey"
+ android:configChanges="keyboardHidden|orientation|keyboard">
+ </activity>
+
+ <activity android:name=".SelectPublicKeyListActivity"
+ android:label="@string/title_selectRecipients"
+ android:configChanges="keyboardHidden|orientation|keyboard">
+ </activity>
+
+ <activity android:name=".SelectSecretKeyListActivity"
+ android:label="@string/title_selectSignature"
+ android:configChanges="keyboardHidden|orientation|keyboard">
+ </activity>
+
+ <activity android:name=".EncryptMessageActivity"
+ android:label="@string/title_encryptMessage"
+ android:configChanges="keyboardHidden|orientation|keyboard">
+ <intent-filter>
+ <action android:name="org.thialfihar.android.apg.intent.ENCRYPT" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".DecryptMessageActivity"
+ android:label="@string/title_decryptMessage"
+ android:configChanges="keyboardHidden|orientation|keyboard">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT"/>
+ <data android:mimeType="text/*"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="org.thialfihar.android.apg.intent.DECRYPT" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".MailListActivity"
+ android:label="@string/title_mailInbox"
+ android:configChanges="keyboardHidden|orientation|keyboard">
+ </activity>
+
+ <provider android:name="org.thialfihar.android.apg.provider.DataProvider"
+ android:authorities="org.thialfihar.android.apg.provider" />
+
+ </application>
+ <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="5" />
+
+<uses-permission android:name="com.google.android.providers.gmail.permission.READ_GMAIL" />
+<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
+</manifest> \ No newline at end of file
diff --git a/default.properties b/default.properties
new file mode 100644
index 000000000..19c96655d
--- /dev/null
+++ b/default.properties
@@ -0,0 +1,13 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "build.properties", and override values to adapt the script to your
+# project structure.
+
+# Indicates whether an apk should be generated for each density.
+split.density=false
+# Project target.
+target=android-4
diff --git a/res/drawable-finger/btn_circle.xml b/res/drawable-finger/btn_circle.xml
new file mode 100644
index 000000000..6c3c7fc1a
--- /dev/null
+++ b/res/drawable-finger/btn_circle.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_window_focused="false" android:state_enabled="true"
+ android:drawable="@drawable/btn_circle_normal" />
+ <item android:state_window_focused="false" android:state_enabled="false"
+ android:drawable="@drawable/btn_circle_disable" />
+ <item android:state_pressed="true"
+ android:drawable="@drawable/btn_circle_pressed" />
+ <item android:state_focused="true" android:state_enabled="true"
+ android:drawable="@drawable/btn_circle_selected" />
+ <item android:state_enabled="true"
+ android:drawable="@drawable/btn_circle_normal" />
+ <item android:state_focused="true"
+ android:drawable="@drawable/btn_circle_disable_focused" />
+ <item
+ android:drawable="@drawable/btn_circle_disable" />
+</selector>
diff --git a/res/drawable-hdpi-finger/btn_circle_disable.png b/res/drawable-hdpi-finger/btn_circle_disable.png
new file mode 100644
index 000000000..ae063b545
--- /dev/null
+++ b/res/drawable-hdpi-finger/btn_circle_disable.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_circle_disable_focused.png b/res/drawable-hdpi-finger/btn_circle_disable_focused.png
new file mode 100644
index 000000000..7a5d4fe4e
--- /dev/null
+++ b/res/drawable-hdpi-finger/btn_circle_disable_focused.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_circle_normal.png b/res/drawable-hdpi-finger/btn_circle_normal.png
new file mode 100644
index 000000000..5eda66883
--- /dev/null
+++ b/res/drawable-hdpi-finger/btn_circle_normal.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_circle_pressed.png b/res/drawable-hdpi-finger/btn_circle_pressed.png
new file mode 100644
index 000000000..88848bac2
--- /dev/null
+++ b/res/drawable-hdpi-finger/btn_circle_pressed.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/btn_circle_selected.png b/res/drawable-hdpi-finger/btn_circle_selected.png
new file mode 100644
index 000000000..74690705f
--- /dev/null
+++ b/res/drawable-hdpi-finger/btn_circle_selected.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_btn_round_minus.png b/res/drawable-hdpi-finger/ic_btn_round_minus.png
new file mode 100644
index 000000000..27af3faf4
--- /dev/null
+++ b/res/drawable-hdpi-finger/ic_btn_round_minus.png
Binary files differ
diff --git a/res/drawable-hdpi-finger/ic_btn_round_plus.png b/res/drawable-hdpi-finger/ic_btn_round_plus.png
new file mode 100644
index 000000000..b24168c32
--- /dev/null
+++ b/res/drawable-hdpi-finger/ic_btn_round_plus.png
Binary files differ
diff --git a/res/drawable-hdpi/encrypted.png b/res/drawable-hdpi/encrypted.png
new file mode 100644
index 000000000..6d7c616a4
--- /dev/null
+++ b/res/drawable-hdpi/encrypted.png
Binary files differ
diff --git a/res/drawable-hdpi/encrypted_large.png b/res/drawable-hdpi/encrypted_large.png
new file mode 100644
index 000000000..dc7466e45
--- /dev/null
+++ b/res/drawable-hdpi/encrypted_large.png
Binary files differ
diff --git a/res/drawable-hdpi/encrypted_small.png b/res/drawable-hdpi/encrypted_small.png
new file mode 100644
index 000000000..5ed9fe4b8
--- /dev/null
+++ b/res/drawable-hdpi/encrypted_small.png
Binary files differ
diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png
new file mode 100644
index 000000000..9e2e7c0e4
--- /dev/null
+++ b/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/res/drawable-hdpi/key.png b/res/drawable-hdpi/key.png
new file mode 100644
index 000000000..6f18c0240
--- /dev/null
+++ b/res/drawable-hdpi/key.png
Binary files differ
diff --git a/res/drawable-hdpi/key_large.png b/res/drawable-hdpi/key_large.png
new file mode 100644
index 000000000..81816835d
--- /dev/null
+++ b/res/drawable-hdpi/key_large.png
Binary files differ
diff --git a/res/drawable-hdpi/key_small.png b/res/drawable-hdpi/key_small.png
new file mode 100644
index 000000000..3f42a0d9b
--- /dev/null
+++ b/res/drawable-hdpi/key_small.png
Binary files differ
diff --git a/res/drawable-hdpi/overlay_error.png b/res/drawable-hdpi/overlay_error.png
new file mode 100644
index 000000000..db6a08329
--- /dev/null
+++ b/res/drawable-hdpi/overlay_error.png
Binary files differ
diff --git a/res/drawable-hdpi/overlay_ok.png b/res/drawable-hdpi/overlay_ok.png
new file mode 100644
index 000000000..33dc08094
--- /dev/null
+++ b/res/drawable-hdpi/overlay_ok.png
Binary files differ
diff --git a/res/drawable-hdpi/signed.png b/res/drawable-hdpi/signed.png
new file mode 100644
index 000000000..92e64dc51
--- /dev/null
+++ b/res/drawable-hdpi/signed.png
Binary files differ
diff --git a/res/drawable-hdpi/signed_large.png b/res/drawable-hdpi/signed_large.png
new file mode 100644
index 000000000..53d8ac991
--- /dev/null
+++ b/res/drawable-hdpi/signed_large.png
Binary files differ
diff --git a/res/drawable-hdpi/signed_small.png b/res/drawable-hdpi/signed_small.png
new file mode 100644
index 000000000..d7f147f05
--- /dev/null
+++ b/res/drawable-hdpi/signed_small.png
Binary files differ
diff --git a/res/drawable-ldpi/encrypted.png b/res/drawable-ldpi/encrypted.png
new file mode 100644
index 000000000..7f4ab803f
--- /dev/null
+++ b/res/drawable-ldpi/encrypted.png
Binary files differ
diff --git a/res/drawable-ldpi/encrypted_large.png b/res/drawable-ldpi/encrypted_large.png
new file mode 100644
index 000000000..2783804bc
--- /dev/null
+++ b/res/drawable-ldpi/encrypted_large.png
Binary files differ
diff --git a/res/drawable-ldpi/encrypted_small.png b/res/drawable-ldpi/encrypted_small.png
new file mode 100644
index 000000000..0ffedf2dd
--- /dev/null
+++ b/res/drawable-ldpi/encrypted_small.png
Binary files differ
diff --git a/res/drawable-ldpi/icon.png b/res/drawable-ldpi/icon.png
new file mode 100644
index 000000000..9d44341f1
--- /dev/null
+++ b/res/drawable-ldpi/icon.png
Binary files differ
diff --git a/res/drawable-ldpi/key.png b/res/drawable-ldpi/key.png
new file mode 100644
index 000000000..121803508
--- /dev/null
+++ b/res/drawable-ldpi/key.png
Binary files differ
diff --git a/res/drawable-ldpi/key_large.png b/res/drawable-ldpi/key_large.png
new file mode 100644
index 000000000..de7e72524
--- /dev/null
+++ b/res/drawable-ldpi/key_large.png
Binary files differ
diff --git a/res/drawable-ldpi/key_small.png b/res/drawable-ldpi/key_small.png
new file mode 100644
index 000000000..1763c4256
--- /dev/null
+++ b/res/drawable-ldpi/key_small.png
Binary files differ
diff --git a/res/drawable-ldpi/overlay_error.png b/res/drawable-ldpi/overlay_error.png
new file mode 100644
index 000000000..568f2b1ee
--- /dev/null
+++ b/res/drawable-ldpi/overlay_error.png
Binary files differ
diff --git a/res/drawable-ldpi/overlay_ok.png b/res/drawable-ldpi/overlay_ok.png
new file mode 100644
index 000000000..db415a846
--- /dev/null
+++ b/res/drawable-ldpi/overlay_ok.png
Binary files differ
diff --git a/res/drawable-ldpi/signed.png b/res/drawable-ldpi/signed.png
new file mode 100644
index 000000000..590220281
--- /dev/null
+++ b/res/drawable-ldpi/signed.png
Binary files differ
diff --git a/res/drawable-ldpi/signed_large.png b/res/drawable-ldpi/signed_large.png
new file mode 100644
index 000000000..490e94fbd
--- /dev/null
+++ b/res/drawable-ldpi/signed_large.png
Binary files differ
diff --git a/res/drawable-ldpi/signed_small.png b/res/drawable-ldpi/signed_small.png
new file mode 100644
index 000000000..ca33fc1f7
--- /dev/null
+++ b/res/drawable-ldpi/signed_small.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_circle_disable.png b/res/drawable-mdpi-finger/btn_circle_disable.png
new file mode 100644
index 000000000..33b74a66c
--- /dev/null
+++ b/res/drawable-mdpi-finger/btn_circle_disable.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_circle_disable_focused.png b/res/drawable-mdpi-finger/btn_circle_disable_focused.png
new file mode 100644
index 000000000..005ad8dca
--- /dev/null
+++ b/res/drawable-mdpi-finger/btn_circle_disable_focused.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_circle_normal.png b/res/drawable-mdpi-finger/btn_circle_normal.png
new file mode 100644
index 000000000..fc5af1c9f
--- /dev/null
+++ b/res/drawable-mdpi-finger/btn_circle_normal.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_circle_pressed.png b/res/drawable-mdpi-finger/btn_circle_pressed.png
new file mode 100644
index 000000000..8f40afdfc
--- /dev/null
+++ b/res/drawable-mdpi-finger/btn_circle_pressed.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/btn_circle_selected.png b/res/drawable-mdpi-finger/btn_circle_selected.png
new file mode 100644
index 000000000..c74fac227
--- /dev/null
+++ b/res/drawable-mdpi-finger/btn_circle_selected.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_btn_round_minus.png b/res/drawable-mdpi-finger/ic_btn_round_minus.png
new file mode 100644
index 000000000..96dbb17d2
--- /dev/null
+++ b/res/drawable-mdpi-finger/ic_btn_round_minus.png
Binary files differ
diff --git a/res/drawable-mdpi-finger/ic_btn_round_plus.png b/res/drawable-mdpi-finger/ic_btn_round_plus.png
new file mode 100644
index 000000000..1ec8a956a
--- /dev/null
+++ b/res/drawable-mdpi-finger/ic_btn_round_plus.png
Binary files differ
diff --git a/res/drawable-mdpi/encrypted.png b/res/drawable-mdpi/encrypted.png
new file mode 100644
index 000000000..2783804bc
--- /dev/null
+++ b/res/drawable-mdpi/encrypted.png
Binary files differ
diff --git a/res/drawable-mdpi/encrypted_large.png b/res/drawable-mdpi/encrypted_large.png
new file mode 100644
index 000000000..6d7c616a4
--- /dev/null
+++ b/res/drawable-mdpi/encrypted_large.png
Binary files differ
diff --git a/res/drawable-mdpi/encrypted_small.png b/res/drawable-mdpi/encrypted_small.png
new file mode 100644
index 000000000..7f4ab803f
--- /dev/null
+++ b/res/drawable-mdpi/encrypted_small.png
Binary files differ
diff --git a/res/drawable-mdpi/icon.png b/res/drawable-mdpi/icon.png
new file mode 100644
index 000000000..b4e4db40e
--- /dev/null
+++ b/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/res/drawable-mdpi/key.png b/res/drawable-mdpi/key.png
new file mode 100644
index 000000000..de7e72524
--- /dev/null
+++ b/res/drawable-mdpi/key.png
Binary files differ
diff --git a/res/drawable-mdpi/key_large.png b/res/drawable-mdpi/key_large.png
new file mode 100644
index 000000000..6f18c0240
--- /dev/null
+++ b/res/drawable-mdpi/key_large.png
Binary files differ
diff --git a/res/drawable-mdpi/key_small.png b/res/drawable-mdpi/key_small.png
new file mode 100644
index 000000000..121803508
--- /dev/null
+++ b/res/drawable-mdpi/key_small.png
Binary files differ
diff --git a/res/drawable-mdpi/overlay_error.png b/res/drawable-mdpi/overlay_error.png
new file mode 100644
index 000000000..2372de59e
--- /dev/null
+++ b/res/drawable-mdpi/overlay_error.png
Binary files differ
diff --git a/res/drawable-mdpi/overlay_ok.png b/res/drawable-mdpi/overlay_ok.png
new file mode 100644
index 000000000..2f0005898
--- /dev/null
+++ b/res/drawable-mdpi/overlay_ok.png
Binary files differ
diff --git a/res/drawable-mdpi/signed.png b/res/drawable-mdpi/signed.png
new file mode 100644
index 000000000..490e94fbd
--- /dev/null
+++ b/res/drawable-mdpi/signed.png
Binary files differ
diff --git a/res/drawable-mdpi/signed_large.png b/res/drawable-mdpi/signed_large.png
new file mode 100644
index 000000000..92e64dc51
--- /dev/null
+++ b/res/drawable-mdpi/signed_large.png
Binary files differ
diff --git a/res/drawable-mdpi/signed_small.png b/res/drawable-mdpi/signed_small.png
new file mode 100644
index 000000000..590220281
--- /dev/null
+++ b/res/drawable-mdpi/signed_small.png
Binary files differ
diff --git a/res/layout/account_item.xml b/res/layout/account_item.xml
new file mode 100644
index 000000000..e37000ff0
--- /dev/null
+++ b/res/layout/account_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:singleLine="true"
+ android:layout_marginRight="?android:attr/scrollbarSize"
+ android:paddingLeft="6dip"
+ android:paddingTop="6dip"
+ android:paddingBottom="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight">
+
+<TextView
+ android:id="@+id/account_name"
+ android:text="someone@gmail.com"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"/>
+
+</LinearLayout>
diff --git a/res/layout/create_key.xml b/res/layout/create_key.xml
new file mode 100644
index 000000000..569b703f5
--- /dev/null
+++ b/res/layout/create_key.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="fill_parent"
+ android:layout_width="fill_parent">
+ <TableLayout
+ android:layout_height="fill_parent"
+ android:layout_width="fill_parent"
+ android:stretchColumns="1"
+ android:layout_marginRight="?android:attr/scrollbarSize"
+ android:paddingLeft="6dip">
+ <TableRow>
+ <TextView android:id="@+id/label_algorithm"
+ android:text="Algorithm"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="10dip"/>
+ <Spinner
+ android:id="@+id/algorithm"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"/>
+ </TableRow>
+ <TableRow>
+ <TextView android:id="@+id/label_size"
+ android:text="Key Size"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="10dip"/>
+ <EditText android:id="@+id/size"
+ android:text="1024"
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent" android:gravity="right" android:numeric="integer"/>
+ </TableRow>
+ </TableLayout>
+</ScrollView>
diff --git a/res/layout/decrypt_message.xml b/res/layout/decrypt_message.xml
new file mode 100644
index 000000000..2a0aa153d
--- /dev/null
+++ b/res/layout/decrypt_message.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:fillViewport="true">
+
+<EditText
+ android:id="@+id/message"
+ android:inputType="text|textCapSentences|textMultiLine|textLongMessage"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:gravity="top"/>
+
+<LinearLayout
+ android:orientation="horizontal"
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:paddingLeft="5dip"
+ android:paddingRight="5dip">
+
+<LinearLayout
+ android:id="@+id/layout_signature"
+ android:orientation="horizontal"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_weight="2">
+
+<RelativeLayout
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content">
+<ImageView
+ android:id="@+id/ic_signature"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/signed_large"/>
+
+<ImageView
+ android:id="@+id/ic_signature_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/overlay_error"/>
+
+</RelativeLayout>
+
+<LinearLayout
+ android:orientation="vertical"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:paddingLeft="5dip">
+
+<TextView
+ android:id="@+id/main_user_id"
+ android:text="Main User Id"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left"/>
+
+<TextView
+ android:id="@+id/main_user_id_rest"
+ android:text="Main User Id Rest"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:layout_gravity="left"/>
+
+</LinearLayout>
+</LinearLayout>
+
+<Button
+ android:id="@+id/btn_decrypt"
+ android:text="@string/btn_decrypt"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"/>
+
+</LinearLayout>
+
+</LinearLayout>
+
diff --git a/res/layout/edit_key.xml b/res/layout/edit_key.xml
new file mode 100644
index 000000000..2fceeb5a3
--- /dev/null
+++ b/res/layout/edit_key.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:paddingTop="5dip"
+ android:fillViewport="true">
+
+<ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+<LinearLayout
+ android:id="@+id/container"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ android:layout_marginRight="?android:attr/scrollbarSize"/>
+</ScrollView>
+
+<LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ style="@android:style/ButtonBar">
+
+ <Button
+ android:id="@+id/btn_save"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_save"/>
+
+ <Button
+ android:id="@+id/btn_discard"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/btn_doNotSave"/>
+</LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/edit_key_key_item.xml b/res/layout/edit_key_key_item.xml
new file mode 100644
index 000000000..46de4a977
--- /dev/null
+++ b/res/layout/edit_key_key_item.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<org.thialfihar.android.apg.ui.widget.KeyEditor
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingLeft="5dip">
+
+<View
+ android:id="@+id/separator"
+ android:layout_width="fill_parent"
+ android:layout_height="1dip"
+ android:background="?android:attr/listDivider"/>
+
+<LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:orientation="horizontal">
+ <TableLayout
+ android:layout_height="wrap_content"
+ android:layout_width="0dip"
+ android:layout_weight="1"
+ android:layout_marginLeft="16dip"
+ android:stretchColumns="1">
+ <TableRow>
+ <TextView android:id="@+id/label_key_id" android:text="Key ID"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="10dip"/>
+ <TextView
+ android:id="@+id/key_id"
+ android:text="00000000 00000000"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:paddingRight="5dip"
+ android:typeface="monospace"/>
+ </TableRow>
+ <TableRow>
+ <TextView android:id="@+id/label_algorithm"
+ android:text="Algorithm"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="10dip"/>
+ <TextView android:id="@+id/algorithm"
+ android:text="Name"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:paddingRight="5dip"/>
+ </TableRow>
+ <TableRow>
+ <TextView android:id="@+id/label_creation"
+ android:text="Creation"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="10dip"/>
+ <TextView
+ android:id="@+id/creation"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"/>
+ </TableRow>
+ <TableRow>
+ <TextView android:id="@+id/label_expiry"
+ android:text="Expiry"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="10dip"/>
+ <Button
+ android:id="@+id/expiry"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"/>
+ </TableRow>
+ <TableRow>
+ <TextView android:id="@+id/label_usage"
+ android:text="Usage"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="10dip"/>
+ <Spinner
+ android:id="@+id/usage"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"/>
+ </TableRow>
+ </TableLayout>
+
+ <ImageButton
+ android:id="@+id/edit_delete"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/MinusButton"
+ android:layout_gravity="center_vertical"/>
+
+</LinearLayout>
+</org.thialfihar.android.apg.ui.widget.KeyEditor>
diff --git a/res/layout/edit_key_section.xml b/res/layout/edit_key_section.xml
new file mode 100644
index 000000000..b3a48f87e
--- /dev/null
+++ b/res/layout/edit_key_section.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<org.thialfihar.android.apg.ui.widget.SectionView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dip"
+ android:background="?android:attr/listDivider"/>
+
+ <LinearLayout
+ android:id="@+id/header"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="14dip"
+ android:layout_marginTop="2dip"
+ android:layout_marginBottom="2dip"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:focusable="true"
+ android:clickable="true">
+
+ <TextView
+ android:id="@+id/title"
+ android:text="Section Name"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"/>
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:duplicateParentState="true"
+ style="@style/PlusButton"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/editors"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="6dip"
+ android:orientation="vertical"/>
+
+</org.thialfihar.android.apg.ui.widget.SectionView>
diff --git a/res/layout/edit_key_user_id_item.xml b/res/layout/edit_key_user_id_item.xml
new file mode 100644
index 000000000..998c436cb
--- /dev/null
+++ b/res/layout/edit_key_user_id_item.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<org.thialfihar.android.apg.ui.widget.UserIdEditor
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingLeft="5dip">
+
+<View
+ android:id="@+id/separator"
+ android:layout_width="fill_parent"
+ android:layout_height="1dip"
+ android:background="?android:attr/listDivider"/>
+
+<RadioButton
+ android:id="@+id/is_main_user_id" android:text="Main User ID"
+ android:layout_height="wrap_content" android:layout_width="wrap_content"
+ android:layout_marginLeft="20dip"/>
+
+<LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:orientation="horizontal">
+ <TableLayout
+ android:layout_height="wrap_content"
+ android:layout_width="0dip"
+ android:layout_weight="1"
+ android:layout_marginLeft="16dip">
+ <TableRow>
+ <TextView
+ android:id="@+id/name_label"
+ android:text="Name"
+ android:layout_gravity="center_vertical"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:paddingRight="5dip"/>
+ <EditText
+ android:id="@+id/name"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"/>
+ </TableRow>
+ <TableRow>
+ <TextView
+ android:id="@+id/email_label"
+ android:text="Email"
+ android:layout_gravity="center_vertical"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:paddingRight="5dip"/>
+ <EditText
+ android:id="@+id/email"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"/>
+ </TableRow>
+ <TableRow>
+ <TextView
+ android:id="@+id/comment_label"
+ android:text="Comment"
+ android:layout_gravity="center_vertical"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:paddingRight="5dip"/>
+ <EditText
+ android:id="@+id/comment"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"/>
+ </TableRow>
+ </TableLayout>
+
+ <ImageButton
+ android:id="@+id/edit_delete"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/MinusButton"
+ android:layout_gravity="center_vertical"/>
+
+</LinearLayout>
+</org.thialfihar.android.apg.ui.widget.UserIdEditor>
diff --git a/res/layout/encrypt_message.xml b/res/layout/encrypt_message.xml
new file mode 100644
index 000000000..254552e03
--- /dev/null
+++ b/res/layout/encrypt_message.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:fillViewport="true">
+
+<EditText
+ android:id="@+id/message"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:gravity="top"
+ android:inputType="text|textCapSentences|textMultiLine|textLongMessage">
+</EditText>
+
+<LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent" android:paddingBottom="3dip">
+
+<CheckBox
+ android:text="@string/sign"
+ android:id="@+id/sign"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"/>
+
+<LinearLayout
+ android:orientation="vertical"
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:layout_gravity="center_vertical"
+ android:paddingRight="5dip">
+
+<TextView
+ android:id="@+id/main_user_id"
+ android:text="Main User Id"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right">
+</TextView>
+
+<TextView
+ android:id="@+id/main_user_id_rest"
+ android:text="Main User Id Rest"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:layout_gravity="right">
+</TextView>
+
+</LinearLayout>
+</LinearLayout>
+
+<LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ style="@android:style/ButtonBar">
+
+<Button
+ android:text="@string/btn_selectEncryptKeys"
+ android:id="@+id/btn_selectEncryptKeys"
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"/>
+
+<Button
+ android:id="@+id/btn_send"
+ android:text="@string/btn_send"
+ android:layout_weight="1"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
+
+</LinearLayout>
+
+
diff --git a/res/layout/key_list_child_item_master_key.xml b/res/layout/key_list_child_item_master_key.xml
new file mode 100644
index 000000000..47eba65b5
--- /dev/null
+++ b/res/layout/key_list_child_item_master_key.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:singleLine="true"
+ android:paddingLeft="10dip"
+ android:layout_marginRight="?android:attr/scrollbarSize"
+ android:layout_height="?android:attr/listPreferredItemHeight" android:layout_width="fill_parent">
+
+<LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:orientation="horizontal"
+ android:paddingRight="3dip">
+
+<ImageView
+ android:id="@+id/ic_master_key"
+ android:src="@drawable/key_small"
+ android:paddingRight="6dip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:layout_gravity="center_vertical"/>
+
+<TextView
+ android:id="@+id/key_id"
+ android:text="Key ID"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:paddingRight="5dip" android:typeface="monospace"/>
+
+<TextView
+ android:id="@+id/key_details"
+ android:text="(RSA, 1024bit)"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+
+<LinearLayout
+ android:gravity="right"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent" android:paddingBottom="2dip" android:paddingTop="2dip" android:layout_height="fill_parent" android:layout_gravity="center_vertical">
+
+<ImageView
+ android:id="@+id/ic_encrypt_key"
+ android:src="@drawable/encrypted_small"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<ImageView
+ android:id="@+id/ic_sign_key"
+ android:src="@drawable/signed_small"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
+
+</LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/key_list_child_item_sub_key.xml b/res/layout/key_list_child_item_sub_key.xml
new file mode 100644
index 000000000..085d78f05
--- /dev/null
+++ b/res/layout/key_list_child_item_sub_key.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:singleLine="true"
+ android:paddingLeft="40dip"
+ android:layout_marginRight="?android:attr/scrollbarSize"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:layout_width="fill_parent">
+
+<LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:orientation="horizontal"
+ android:paddingRight="3dip">
+
+<TextView
+ android:id="@+id/key_id"
+ android:text="Key ID"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:paddingRight="5dip" android:typeface="monospace"/>
+
+<TextView
+ android:id="@+id/key_details"
+ android:text="(RSA, 1024bit)"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+
+<LinearLayout
+ android:gravity="right"
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:paddingBottom="2dip"
+ android:paddingTop="2dip"
+ android:layout_gravity="center_vertical">
+
+<ImageView
+ android:id="@+id/ic_encrypt_key"
+ android:src="@drawable/encrypted_small"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<ImageView
+ android:id="@+id/ic_sign_key"
+ android:src="@drawable/signed_small"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
+
+</LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/key_list_child_item_user_id.xml b/res/layout/key_list_child_item_user_id.xml
new file mode 100644
index 000000000..80cdd2867
--- /dev/null
+++ b/res/layout/key_list_child_item_user_id.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:paddingLeft="40dip"
+ android:layout_marginRight="?android:attr/scrollbarSize"
+ android:singleLine="true"
+ android:layout_width="fill_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight">
+
+<TextView
+ android:id="@+id/user_id"
+ android:text="User ID"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingRight="3dip"/>
+
+</LinearLayout>
diff --git a/res/layout/key_list_group_item.xml b/res/layout/key_list_group_item.xml
new file mode 100644
index 000000000..aaada82e3
--- /dev/null
+++ b/res/layout/key_list_group_item.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:singleLine="true"
+ android:layout_marginRight="?android:attr/scrollbarSize"
+ android:layout_width="fill_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight">
+
+<LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" android:paddingLeft="36dip">
+
+<TextView
+ android:id="@+id/main_user_id"
+ android:text="Main User ID"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<TextView
+ android:id="@+id/main_user_id_rest"
+ android:text="&lt;user@somewhere.com&gt;"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
+
+<LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+</LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/mailbox_message_item.xml b/res/layout/mailbox_message_item.xml
new file mode 100644
index 000000000..b2b5e91d4
--- /dev/null
+++ b/res/layout/mailbox_message_item.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:singleLine="true"
+ android:paddingLeft="3dip"
+ android:layout_marginRight="?android:attr/scrollbarSize"
+ android:paddingTop="3dip"
+ android:paddingBottom="3dip"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:layout_width="fill_parent">
+
+<ImageView
+ android:id="@+id/ic_encrypted"
+ android:src="@drawable/encrypted"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"/>
+
+<LinearLayout
+ android:orientation="vertical"
+ android:paddingLeft="5dip"
+ android:layout_weight="1"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+<TextView
+ android:id="@+id/subject"
+ android:text="Subject"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<TextView
+ android:id="@+id/email_address"
+ android:text="user@somewhere.com"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/main.xml b/res/layout/main.xml
new file mode 100644
index 000000000..81c5f224b
--- /dev/null
+++ b/res/layout/main.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:paddingTop="5dip"
+ android:fillViewport="true">
+
+<ScrollView
+ android:layout_marginTop="10dip"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+
+<ListView
+ android:id="@+id/account_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+</ListView>
+
+</ScrollView>
+
+<LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ style="@android:style/ButtonBar">
+
+<Button
+ android:id="@+id/btn_encryptMessage"
+ android:text="@string/btn_encryptMessage"
+ android:layout_width="wrap_content"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"/>
+
+<Button
+ android:id="@+id/btn_decryptMessage"
+ android:text="@string/btn_decryptMessage"
+ android:layout_width="wrap_content"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/select_public_key.xml b/res/layout/select_public_key.xml
new file mode 100644
index 000000000..9a2d9f578
--- /dev/null
+++ b/res/layout/select_public_key.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:fillViewport="true">
+
+<ListView
+ android:id="@+id/list"
+ android:choiceMode="multipleChoice"
+ android:layout_width="fill_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+</ListView>
+
+<LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ style="@android:style/ButtonBar">
+
+<Button
+ android:text="@android:string/ok"
+ android:id="@+id/btn_ok"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"/>
+
+<Button
+ android:text="@android:string/cancel"
+ android:id="@+id/btn_cancel"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"/>
+
+</LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/select_public_key_item.xml b/res/layout/select_public_key_item.xml
new file mode 100644
index 000000000..aba0c09b9
--- /dev/null
+++ b/res/layout/select_public_key_item.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:singleLine="true"
+ android:paddingLeft="3dip"
+ android:paddingRight="?android:attr/scrollbarSize"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:layout_width="fill_parent">
+
+<CheckBox
+ android:id="@+id/selected"
+ android:focusable="false"
+ android:focusableInTouchMode="false"
+ android:clickable="false"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<LinearLayout
+ android:orientation="vertical"
+ android:paddingLeft="5dip"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1">
+
+<TextView
+ android:id="@+id/main_user_id"
+ android:text="Main User ID"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<TextView
+ android:id="@+id/main_user_id_rest"
+ android:text="&lt;user@somewhere.com&gt;"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+</LinearLayout>
+
+<LinearLayout
+ android:id="@+id/right_column"
+ android:orientation="vertical"
+ android:minWidth="90dip"
+ android:paddingLeft="3dip"
+ android:gravity="right"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+<TextView
+ android:id="@+id/key_id"
+ android:text="BBBBBBBB"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:typeface="monospace"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"/>
+
+<TextView
+ android:id="@+id/creation"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:text="31.12.2009"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<TextView
+ android:id="@+id/expiry"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:text="31.12.2010"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<TextView
+ android:id="@+id/status"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:text="expired"
+ android:textStyle="italic"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/select_secret_key.xml b/res/layout/select_secret_key.xml
new file mode 100644
index 000000000..64967ace6
--- /dev/null
+++ b/res/layout/select_secret_key.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:fillViewport="true">
+
+<ListView
+ android:id="@+id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+</ListView>
+
+</LinearLayout>
diff --git a/res/layout/select_secret_key_item.xml b/res/layout/select_secret_key_item.xml
new file mode 100644
index 000000000..0b0475c37
--- /dev/null
+++ b/res/layout/select_secret_key_item.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:singleLine="true"
+ android:paddingLeft="3dip"
+ android:paddingRight="?android:attr/scrollbarSize"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:layout_width="fill_parent">
+
+<LinearLayout
+ android:orientation="vertical"
+ android:paddingLeft="5dip"
+ android:paddingRight="5dip"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1">
+
+<TextView
+ android:id="@+id/main_user_id"
+ android:text="Main User ID"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<TextView
+ android:id="@+id/main_user_id_rest"
+ android:text="&lt;user@somewhere.com&gt;"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+</LinearLayout>
+
+<LinearLayout
+ android:orientation="vertical"
+ android:minWidth="90dip"
+ android:paddingLeft="3dip"
+ android:gravity="right"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+<TextView
+ android:id="@+id/key_id"
+ android:text="BBBBBBBB"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:typeface="monospace"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"/>
+
+<TextView
+ android:id="@+id/creation"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:text="31.12.2009"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<TextView
+ android:id="@+id/expiry"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:text="31.12.2010"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+<TextView
+ android:id="@+id/status"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:text="expired"
+ android:textStyle="italic"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 000000000..b1fa76915
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources>
+ <string name="app_name">APG</string>
+ <string name="title_mailInbox">Mail Inbox</string>
+ <string name="title_managePublicKeys">Manage Public Keys</string>
+ <string name="title_manageSecretKeys">Manage Secret Keys</string>
+ <string name="title_selectRecipients">Select Recipients</string>
+ <string name="title_selectSignature">Select Signature</string>
+ <string name="title_encryptMessage">Encrypt Message</string>
+ <string name="title_decryptMessage">Decrypt Message</string>
+ <string name="title_authentification">Authentification</string>
+ <string name="title_createKey">Create Key</string>
+ <string name="title_editKey">Edit Key</string>
+
+ <string name="section_userIds">User IDs</string>
+ <string name="section_keys">Keys</string>
+
+ <string name="btn_send">Send via Email</string>
+ <string name="btn_decrypt">Decrypt</string>
+ <string name="btn_selectEncryptKeys">Select Recipients</string>
+ <string name="btn_reply">Reply</string>
+ <string name="btn_encryptMessage">Encrypt Message</string>
+ <string name="btn_decryptMessage">Decrypt Message</string>
+ <string name="btn_save">Save</string>
+ <string name="btn_doNotSave">Cancel</string>
+
+ <string name="menu_about">About</string>
+ <string name="menu_addAccount">Add GMail Account</string>
+ <string name="menu_managePublicKeys">Manage Public Keys</string>
+ <string name="menu_manageSecretKeys">Manage Secret Keys</string>
+
+ <string name="sign">Sign</string>
+ <string name="sign_as">Sign as</string>
+ <string name="no_keys_selected">Select Recipients</string>
+ <string name="one_key_selected">1 Recipient</string>
+ <string name="n_keys_selected">Recipients</string>
+ <string name="unknown_user_id">&lt;unknown&gt;</string>
+ <string name="none">&lt;none&gt;</string>
+
+ <string name="sign_only">Sign only</string>
+ <string name="encrypt_only">Encrypt only</string>
+ <string name="sign_and_encrypt">Sign and Encrypt</string>
+
+ <string name="dsa">DSA</string>
+ <string name="elgamal">ElGamal</string>
+ <string name="rsa">RSA</string>
+
+ <string name="wrong_pass_phrase">Wrong pass phrase.</string>
+ <string name="using_clipboard_content">Using clipboard content.</string>
+ <string name="key_saved">Key saved.</string>
+ <string name="set_a_pass_phrase">Set a pass phrase via the option menu first.</string>
+</resources>
+
diff --git a/res/values/styles.xml b/res/values/styles.xml
new file mode 100644
index 000000000..720f7aedb
--- /dev/null
+++ b/res/values/styles.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources>
+ <style name="MinusButton">
+ <item name="android:background">@drawable/btn_circle</item>
+ <item name="android:src">@drawable/ic_btn_round_minus</item>
+ </style>
+
+ <style name="PlusButton">
+ <item name="android:background">@drawable/btn_circle</item>
+ <item name="android:src">@drawable/ic_btn_round_plus</item>
+ </style>
+</resources>
diff --git a/src/org/thialfihar/android/apg/Apg.java b/src/org/thialfihar/android/apg/Apg.java
new file mode 100644
index 000000000..68c4f8877
--- /dev/null
+++ b/src/org/thialfihar/android/apg/Apg.java
@@ -0,0 +1,1496 @@
+/*
+ * 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.thialfihar.android.apg;
+
+import java.io.BufferedOutputStream;
+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.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.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Vector;
+import java.util.regex.Pattern;
+
+import org.bouncycastle2.bcpg.ArmoredOutputStream;
+import org.bouncycastle2.bcpg.BCPGOutputStream;
+import org.bouncycastle2.bcpg.CompressionAlgorithmTags;
+import org.bouncycastle2.bcpg.HashAlgorithmTags;
+import org.bouncycastle2.bcpg.SymmetricKeyAlgorithmTags;
+import org.bouncycastle2.bcpg.sig.KeyFlags;
+import org.bouncycastle2.jce.provider.BouncyCastleProvider;
+import org.bouncycastle2.jce.spec.ElGamalParameterSpec;
+import org.bouncycastle2.openpgp.PGPCompressedData;
+import org.bouncycastle2.openpgp.PGPCompressedDataGenerator;
+import org.bouncycastle2.openpgp.PGPEncryptedData;
+import org.bouncycastle2.openpgp.PGPEncryptedDataGenerator;
+import org.bouncycastle2.openpgp.PGPEncryptedDataList;
+import org.bouncycastle2.openpgp.PGPException;
+import org.bouncycastle2.openpgp.PGPKeyPair;
+import org.bouncycastle2.openpgp.PGPKeyRingGenerator;
+import org.bouncycastle2.openpgp.PGPLiteralData;
+import org.bouncycastle2.openpgp.PGPLiteralDataGenerator;
+import org.bouncycastle2.openpgp.PGPObjectFactory;
+import org.bouncycastle2.openpgp.PGPOnePassSignature;
+import org.bouncycastle2.openpgp.PGPOnePassSignatureList;
+import org.bouncycastle2.openpgp.PGPPrivateKey;
+import org.bouncycastle2.openpgp.PGPPublicKey;
+import org.bouncycastle2.openpgp.PGPPublicKeyEncryptedData;
+import org.bouncycastle2.openpgp.PGPPublicKeyRing;
+import org.bouncycastle2.openpgp.PGPSecretKey;
+import org.bouncycastle2.openpgp.PGPSecretKeyRing;
+import org.bouncycastle2.openpgp.PGPSignature;
+import org.bouncycastle2.openpgp.PGPSignatureGenerator;
+import org.bouncycastle2.openpgp.PGPSignatureList;
+import org.bouncycastle2.openpgp.PGPSignatureSubpacketGenerator;
+import org.bouncycastle2.openpgp.PGPSignatureSubpacketVector;
+import org.bouncycastle2.openpgp.PGPUtil;
+import org.thialfihar.android.apg.provider.PublicKeys;
+import org.thialfihar.android.apg.provider.SecretKeys;
+import org.thialfihar.android.apg.ui.widget.KeyEditor;
+import org.thialfihar.android.apg.ui.widget.SectionView;
+import org.thialfihar.android.apg.ui.widget.UserIdEditor;
+import org.thialfihar.android.apg.utils.IterableIterator;
+
+import android.app.Activity;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.ViewGroup;
+
+public class Apg {
+ public static class Intent {
+ public static final String DECRYPT = "org.thialfihar.android.apg.intent.DECRYPT";
+ public static final String ENCRYPT = "org.thialfihar.android.apg.intent.ENCRYPT";
+ }
+
+ public static String VERSION = "0.8.0";
+ public static String FULL_VERSION = "APG v" + VERSION;
+
+ 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 };
+
+ protected static Vector<PGPPublicKeyRing> mPublicKeyRings;
+ protected static Vector<PGPSecretKeyRing> mSecretKeyRings;
+
+ public static Pattern PGP_MESSAGE =
+ Pattern.compile(".*?(-----BEGIN PGP MESSAGE-----\n.*?-----END PGP MESSAGE-----).*",
+ Pattern.DOTALL);
+
+ protected static boolean mInitialized = false;
+
+ protected static final int RETURN_NO_MASTER_KEY = -2;
+ protected static final int RETURN_ERROR = -1;
+ protected static final int RETURN_OK = 0;
+ protected static final int RETURN_UPDATED = 1;
+
+ protected static final int TYPE_PUBLIC = 0;
+ protected static final int TYPE_SECRET = 1;
+
+ protected static HashMap<Long, Integer> mSecretKeyIdToIdMap;
+ protected static HashMap<Long, PGPSecretKeyRing> mSecretKeyIdToKeyRingMap;
+ protected static HashMap<Long, Integer> mPublicKeyIdToIdMap;
+ protected static HashMap<Long, PGPPublicKeyRing> mPublicKeyIdToKeyRingMap;
+
+ public static final String PUBLIC_KEY_PROJECTION[] =
+ new String[] {
+ PublicKeys._ID,
+ PublicKeys.KEY_ID,
+ PublicKeys.KEY_DATA,
+ PublicKeys.WHO_ID, };
+ public static final String SECRET_KEY_PROJECTION[] =
+ new String[] {
+ PublicKeys._ID,
+ PublicKeys.KEY_ID,
+ PublicKeys.KEY_DATA,
+ PublicKeys.WHO_ID, };
+
+ private static String mPassPhrase = null;
+
+ public static class GeneralException extends Exception {
+ static final long serialVersionUID = 0xf812773342L;
+
+ public GeneralException(String message) {
+ super(message);
+ }
+ }
+
+ static {
+ mPublicKeyRings = new Vector<PGPPublicKeyRing>();
+ mSecretKeyRings = new Vector<PGPSecretKeyRing>();
+ mSecretKeyIdToIdMap = new HashMap<Long, Integer>();
+ mSecretKeyIdToKeyRingMap = new HashMap<Long, PGPSecretKeyRing>();
+ mPublicKeyIdToIdMap = new HashMap<Long, Integer>();
+ mPublicKeyIdToKeyRingMap = new HashMap<Long, PGPPublicKeyRing>();
+ }
+
+ public static void initialize(Activity context) {
+ setPassPhrase(null);
+ if (mInitialized) {
+ return;
+ }
+
+ loadKeyRings(context, TYPE_PUBLIC);
+ loadKeyRings(context, TYPE_SECRET);
+
+ mInitialized = true;
+ }
+
+ public static class PublicKeySorter implements Comparator<PGPPublicKeyRing> {
+ @Override
+ public int compare(PGPPublicKeyRing object1, PGPPublicKeyRing object2) {
+ PGPPublicKey key1 = getMasterKey(object1);
+ PGPPublicKey key2 = getMasterKey(object2);
+ if (key1 == null && key2 == null) {
+ return 0;
+ }
+
+ if (key1 == null) {
+ return -1;
+ }
+
+ if (key2 == null) {
+ return 1;
+ }
+
+ String uid1 = getMainUserId(key1);
+ String uid2 = getMainUserId(key2);
+ if (uid1 == null && uid2 == null) {
+ return 0;
+ }
+
+ if (uid1 == null) {
+ return -1;
+ }
+
+ if (uid2 == null) {
+ return 1;
+ }
+
+ return uid1.compareTo(uid2);
+ }
+ }
+
+ public static class SecretKeySorter implements Comparator<PGPSecretKeyRing> {
+ @Override
+ public int compare(PGPSecretKeyRing object1, PGPSecretKeyRing object2) {
+ PGPSecretKey key1 = getMasterKey(object1);
+ PGPSecretKey key2 = getMasterKey(object2);
+ if (key1 == null && key2 == null) {
+ return 0;
+ }
+
+ if (key1 == null) {
+ return -1;
+ }
+
+ if (key2 == null) {
+ return 1;
+ }
+
+ String uid1 = getMainUserId(key1);
+ String uid2 = getMainUserId(key2);
+ if (uid1 == null && uid2 == null) {
+ return 0;
+ }
+
+ if (uid1 == null) {
+ return -1;
+ }
+
+ if (uid2 == null) {
+ return 1;
+ }
+
+ return uid1.compareTo(uid2);
+ }
+ }
+
+ public static void setPassPhrase(String passPhrase) {
+ mPassPhrase = passPhrase;
+ }
+
+ public static String getPassPhrase() {
+ return mPassPhrase;
+ }
+
+ public static PGPSecretKey createKey(KeyEditor.AlgorithmChoice algorithmChoice, int keySize,
+ String passPhrase)
+ throws NoSuchAlgorithmException, PGPException, NoSuchProviderException,
+ GeneralException, InvalidAlgorithmParameterException {
+
+ if (algorithmChoice == null) {
+ throw new GeneralException("unknown algorithm choice");
+ }
+ if (keySize < 512) {
+ throw new GeneralException("key size must be at least 512bit");
+ }
+
+ Security.addProvider(new BouncyCastleProvider());
+
+ if (passPhrase == null) {
+ passPhrase = "";
+ }
+
+ int algorithm = 0;
+ KeyPairGenerator keyGen = null;
+
+ switch (algorithmChoice.getId()) {
+ case KeyEditor.AlgorithmChoice.DSA: {
+ keyGen = KeyPairGenerator.getInstance("DSA", new BouncyCastleProvider());
+ keyGen.initialize(keySize, new SecureRandom());
+ algorithm = PGPPublicKey.DSA;
+ break;
+ }
+
+ case KeyEditor.AlgorithmChoice.ELGAMAL: {
+ if (keySize != 2048) {
+ throw new GeneralException("ElGamal currently requires 2048bit");
+ }
+ keyGen = KeyPairGenerator.getInstance("ELGAMAL", new BouncyCastleProvider());
+ BigInteger p = new BigInteger(
+ "36F0255DDE973DCB3B399D747F23E32ED6FDB1F77598338BFDF44159C4EC64DDAEB5F78671CBFB22" +
+ "106AE64C32C5BCE4CFD4F5920DA0EBC8B01ECA9292AE3DBA1B7A4A899DA181390BB3BD1659C81294" +
+ "F400A3490BF9481211C79404A576605A5160DBEE83B4E019B6D799AE131BA4C23DFF83475E9C40FA" +
+ "6725B7C9E3AA2C6596E9C05702DB30A07C9AA2DC235C5269E39D0CA9DF7AAD44612AD6F88F696992" +
+ "98F3CAB1B54367FB0E8B93F735E7DE83CD6FA1B9D1C931C41C6188D3E7F179FC64D87C5D13F85D70" +
+ "4A3AA20F90B3AD3621D434096AA7E8E7C66AB683156A951AEA2DD9E76705FAEFEA8D71A575535597" +
+ "0000000000000001", 16);
+ ElGamalParameterSpec elParams = new ElGamalParameterSpec(p, new BigInteger("2"));
+ keyGen.initialize(elParams);
+ algorithm = PGPPublicKey.ELGAMAL_GENERAL;
+ break;
+ }
+
+ case KeyEditor.AlgorithmChoice.RSA: {
+ keyGen = KeyPairGenerator.getInstance("RSA", new BouncyCastleProvider());
+ keyGen.initialize(keySize, new SecureRandom());
+
+ algorithm = PGPPublicKey.RSA_GENERAL;
+ break;
+ }
+
+ default: {
+ throw new GeneralException("unknown algorithm choice");
+ }
+ }
+
+ PGPKeyPair keyPair = new PGPKeyPair(algorithm, keyGen.generateKeyPair(), new Date());
+
+ // enough for now, as we assemble the key again later anyway
+ PGPSecretKey secretKey =
+ new PGPSecretKey(PGPSignature.DEFAULT_CERTIFICATION, keyPair, "",
+ PGPEncryptedData.CAST5, passPhrase.toCharArray(), null, null,
+ new SecureRandom(), new BouncyCastleProvider().getName());
+
+ return secretKey;
+ }
+
+ private static long getNumDatesBetween(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;
+ }
+
+ public static void buildSecretKey(Activity context,
+ SectionView userIdsView, SectionView keysView,
+ String oldPassPhrase, String newPassPhrase,
+ ProgressDialogUpdater progress)
+ throws Apg.GeneralException, NoSuchProviderException, PGPException,
+ NoSuchAlgorithmException, SignatureException {
+
+ progress.setProgress("building key...", 0, 100);
+
+ Security.addProvider(new BouncyCastleProvider());
+
+ if (oldPassPhrase == null || oldPassPhrase.equals("")) {
+ oldPassPhrase = "";
+ }
+
+ if (newPassPhrase == null || newPassPhrase.equals("")) {
+ newPassPhrase = "";
+ }
+
+ Vector<String> userIds = new Vector<String>();
+ Vector<PGPSecretKey> keys = new Vector<PGPSecretKey>();
+
+ ViewGroup userIdEditors = userIdsView.getEditors();
+ ViewGroup keyEditors = keysView.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 Apg.GeneralException("you need to specify a name");
+ } catch (UserIdEditor.NoEmailException e) {
+ throw new Apg.GeneralException("you need to specify an email");
+ } catch (UserIdEditor.InvalidEmailException e) {
+ throw new Apg.GeneralException(e.getMessage());
+ }
+
+ if (userId.equals("")) {
+ continue;
+ }
+
+ if (editor.isMainUserId()) {
+ userIds.insertElementAt(userId, 0);
+ gotMainUserId = true;
+ } else {
+ userIds.add(userId);
+ }
+ }
+
+ if (userIds.size() == 0) {
+ throw new Apg.GeneralException("need at least one user id");
+ }
+
+ if (!gotMainUserId) {
+ throw new Apg.GeneralException("main user id can't be empty");
+ }
+
+ if (keyEditors.getChildCount() == 0) {
+ throw new Apg.GeneralException("need at least a main key");
+ }
+
+ for (int i = 0; i < keyEditors.getChildCount(); ++i) {
+ KeyEditor editor = (KeyEditor)keyEditors.getChildAt(i);
+ keys.add(editor.getValue());
+ }
+
+ progress.setProgress("preparing master key...", 10, 100);
+ KeyEditor keyEditor = (KeyEditor) keyEditors.getChildAt(0);
+ int usageId = keyEditor.getUsage().getId();
+ boolean canSign = (usageId == KeyEditor.UsageChoice.SIGN_ONLY ||
+ usageId == KeyEditor.UsageChoice.SIGN_AND_ENCRYPT);
+ boolean canEncrypt = (usageId == KeyEditor.UsageChoice.ENCRYPT_ONLY ||
+ usageId == KeyEditor.UsageChoice.SIGN_AND_ENCRYPT);
+
+ String mainUserId = userIds.get(0);
+
+ PGPSecretKey masterKey = keys.get(0);
+ PGPPublicKey tmpKey = masterKey.getPublicKey();
+ PGPPublicKey masterPublicKey =
+ new PGPPublicKey(tmpKey.getAlgorithm(),
+ tmpKey.getKey(new BouncyCastleProvider()),
+ tmpKey.getCreationTime());
+ PGPPrivateKey masterPrivateKey =
+ masterKey.extractPrivateKey(oldPassPhrase.toCharArray(),
+ new BouncyCastleProvider());
+
+ progress.setProgress("certifying master key...", 20, 100);
+ for (int i = 0; i < userIds.size(); ++i) {
+ String userId = userIds.get(i);
+
+ PGPSignatureGenerator sGen =
+ new PGPSignatureGenerator(masterPublicKey.getAlgorithm(),
+ HashAlgorithmTags.SHA1, new BouncyCastleProvider());
+
+ sGen.initSign(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
+
+ 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);
+
+ if (keyEditor.getExpiryDate() != null) {
+ GregorianCalendar creationDate = new GregorianCalendar();
+ creationDate.setTime(getCreationDate(masterKey));
+ GregorianCalendar expiryDate = keyEditor.getExpiryDate();
+ long numDays = getNumDatesBetween(creationDate, expiryDate);
+ if (numDays <= 0) {
+ throw new GeneralException("expiry date must be later than creation date");
+ }
+ hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400);
+ }
+
+ progress.setProgress("building master key ring...", 30, 100);
+ PGPKeyRingGenerator keyGen =
+ new PGPKeyRingGenerator(PGPSignature.DEFAULT_CERTIFICATION,
+ masterKeyPair, mainUserId,
+ PGPEncryptedData.CAST5, newPassPhrase.toCharArray(),
+ hashedPacketsGen.generate(), unhashedPacketsGen.generate(),
+ new SecureRandom(), new BouncyCastleProvider().getName());
+
+ progress.setProgress("adding sub keys...", 40, 100);
+ for (int i = 1; i < keys.size(); ++i) {
+ progress.setProgress(40 + 50 * (i - 1)/ (keys.size() - 1), 100);
+ PGPSecretKey subKey = keys.get(i);
+ keyEditor = (KeyEditor) keyEditors.getChildAt(i);
+ PGPPublicKey subPublicKey = subKey.getPublicKey();
+ PGPPrivateKey subPrivateKey =
+ subKey.extractPrivateKey(oldPassPhrase.toCharArray(),
+ new BouncyCastleProvider());
+ PGPKeyPair subKeyPair =
+ new PGPKeyPair(subPublicKey.getAlgorithm(),
+ subPublicKey.getKey(new BouncyCastleProvider()),
+ subPrivateKey.getKey(),
+ subPublicKey.getCreationTime());
+
+ hashedPacketsGen = new PGPSignatureSubpacketGenerator();
+ unhashedPacketsGen = new PGPSignatureSubpacketGenerator();
+
+ keyFlags = 0;
+ usageId = keyEditor.getUsage().getId();
+ canSign = (usageId == KeyEditor.UsageChoice.SIGN_ONLY ||
+ usageId == KeyEditor.UsageChoice.SIGN_AND_ENCRYPT);
+ canEncrypt = (usageId == KeyEditor.UsageChoice.ENCRYPT_ONLY ||
+ usageId == KeyEditor.UsageChoice.SIGN_AND_ENCRYPT);
+ if (canSign) {
+ keyFlags |= KeyFlags.SIGN_DATA;
+ }
+ if (canEncrypt) {
+ keyFlags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
+ }
+ hashedPacketsGen.setKeyFlags(true, keyFlags);
+
+ if (keyEditor.getExpiryDate() != null) {
+ GregorianCalendar creationDate = new GregorianCalendar();
+ creationDate.setTime(getCreationDate(masterKey));
+ GregorianCalendar expiryDate = keyEditor.getExpiryDate();
+ long numDays = getNumDatesBetween(creationDate, expiryDate);
+ if (numDays <= 0) {
+ throw new GeneralException("expiry date must be later than creation date");
+ }
+ hashedPacketsGen.setKeyExpirationTime(true, numDays * 86400);
+ }
+
+ keyGen.addSubKey(subKeyPair,
+ hashedPacketsGen.generate(), unhashedPacketsGen.generate());
+ }
+
+ PGPSecretKeyRing secretKeyRing = keyGen.generateSecretKeyRing();
+ PGPPublicKeyRing publicKeyRing = keyGen.generatePublicKeyRing();
+
+ progress.setProgress("saving key ring...", 90, 100);
+ saveKeyRing(context, secretKeyRing);
+ saveKeyRing(context, publicKeyRing);
+
+ loadKeyRings(context, TYPE_PUBLIC);
+ loadKeyRings(context, TYPE_SECRET);
+ progress.setProgress("done.", 100, 100);
+ }
+
+ private static int saveKeyRing(Activity context, PGPPublicKeyRing keyRing) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ContentValues values = new ContentValues();
+
+ PGPPublicKey masterKey = getMasterKey(keyRing);
+ if (masterKey == null) {
+ return RETURN_NO_MASTER_KEY;
+ }
+
+ try {
+ keyRing.encode(out);
+ out.close();
+ } catch (IOException e) {
+ return RETURN_ERROR;
+ }
+
+ values.put(PublicKeys.KEY_ID, masterKey.getKeyID());
+ values.put(PublicKeys.KEY_DATA, out.toByteArray());
+
+ Uri uri = Uri.withAppendedPath(PublicKeys.CONTENT_URI_BY_KEY_ID, "" + masterKey.getKeyID());
+ Cursor cursor = context.managedQuery(uri, PUBLIC_KEY_PROJECTION, null, null, null);
+ if (cursor != null && cursor.getCount() > 0) {
+ context.getContentResolver().update(uri, values, null, null);
+ return RETURN_UPDATED;
+ } else {
+ context.getContentResolver().insert(PublicKeys.CONTENT_URI, values);
+ return RETURN_OK;
+ }
+ }
+
+ private static int saveKeyRing(Activity context, PGPSecretKeyRing keyRing) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ContentValues values = new ContentValues();
+
+ PGPSecretKey masterKey = getMasterKey(keyRing);
+ if (masterKey == null) {
+ return RETURN_NO_MASTER_KEY;
+ }
+
+ try {
+ keyRing.encode(out);
+ out.close();
+ } catch (IOException e) {
+ return RETURN_ERROR;
+ }
+
+ values.put(SecretKeys.KEY_ID, masterKey.getKeyID());
+ values.put(SecretKeys.KEY_DATA, out.toByteArray());
+
+ Uri uri = Uri.withAppendedPath(SecretKeys.CONTENT_URI_BY_KEY_ID, "" + masterKey.getKeyID());
+ Cursor cursor = context.managedQuery(uri, SECRET_KEY_PROJECTION, null, null, null);
+ if (cursor != null && cursor.getCount() > 0) {
+ context.getContentResolver().update(uri, values, null, null);
+ return RETURN_UPDATED;
+ } else {
+ context.getContentResolver().insert(SecretKeys.CONTENT_URI, values);
+ return RETURN_OK;
+ }
+ }
+
+ public static Bundle importKeyRings(Activity context, int type, String filename,
+ ProgressDialogUpdater progress)
+ throws GeneralException, FileNotFoundException, PGPException, IOException {
+ Bundle returnData = new Bundle();
+ PGPObjectFactory objectFactors = null;
+
+ if (type == TYPE_SECRET) {
+ progress.setProgress("importing secret keys...", 0, 100);
+ } else {
+ progress.setProgress("importing public keys...", 0, 100);
+ }
+
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ throw new GeneralException("external storage not ready");
+ }
+
+ FileInputStream fileIn = new FileInputStream(filename);
+ InputStream in = PGPUtil.getDecoderStream(fileIn);
+ objectFactors = new PGPObjectFactory(in);
+
+ Vector<Object> objects = new Vector<Object>();
+ Object obj = objectFactors.nextObject();
+ while (obj != null) {
+ objects.add(obj);
+ obj = objectFactors.nextObject();
+ }
+
+ int newKeys = 0;
+ int oldKeys = 0;
+ for (int i = 0; i < objects.size(); ++i) {
+ progress.setProgress(i * 100 / objects.size(), 100);
+ obj = objects.get(i);
+ PGPPublicKeyRing publicKeyRing;
+ PGPSecretKeyRing secretKeyRing;
+ int retValue;
+
+ if (type == TYPE_SECRET) {
+ if (!(obj instanceof PGPSecretKeyRing)) {
+ continue;
+ }
+ secretKeyRing = (PGPSecretKeyRing) obj;
+ retValue = saveKeyRing(context, secretKeyRing);
+ } else {
+ if (!(obj instanceof PGPPublicKeyRing)) {
+ continue;
+ }
+ publicKeyRing = (PGPPublicKeyRing) obj;
+ retValue = saveKeyRing(context, publicKeyRing);
+ }
+
+ if (retValue == RETURN_ERROR) {
+ throw new GeneralException("error saving some key(s)");
+ }
+
+ if (retValue == RETURN_UPDATED) {
+ ++oldKeys;
+ } else if (retValue == RETURN_OK) {
+ ++newKeys;
+ }
+ }
+
+ progress.setProgress("reloading keys...", 100, 100);
+ loadKeyRings(context, type);
+
+ returnData.putInt("added", newKeys);
+ returnData.putInt("updated", oldKeys);
+
+ progress.setProgress("done.", 100, 100);
+
+ return returnData;
+ }
+
+ public static Bundle exportKeyRings(Activity context, Vector<Object> keys, String filename,
+ ProgressDialogUpdater progress)
+ throws GeneralException, FileNotFoundException, PGPException, IOException {
+ Bundle returnData = new Bundle();
+
+ if (keys.size() == 1) {
+ progress.setProgress("exporting key...", 0, 100);
+ } else {
+ progress.setProgress("exporting keys...", 0, 100);
+ }
+
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ throw new GeneralException("external storage not ready");
+ }
+ FileOutputStream fileOut = new FileOutputStream(new File(filename), false);
+ ArmoredOutputStream out = new ArmoredOutputStream(fileOut);
+
+ int numKeys = 0;
+ for (int i = 0; i < keys.size(); ++i) {
+ progress.setProgress(i * 100 / keys.size(), 100);
+ Object obj = keys.get(i);
+ PGPPublicKeyRing publicKeyRing;
+ PGPSecretKeyRing secretKeyRing;
+
+ if (obj instanceof PGPSecretKeyRing) {
+ secretKeyRing = (PGPSecretKeyRing) obj;
+ secretKeyRing.encode(out);
+ } else if (obj instanceof PGPPublicKeyRing) {
+ publicKeyRing = (PGPPublicKeyRing) obj;
+ publicKeyRing.encode(out);
+ } else {
+ continue;
+ }
+ ++numKeys;
+ }
+ out.close();
+ fileOut.close();
+ returnData.putInt("exported", numKeys);
+
+ progress.setProgress("done.", 100, 100);
+
+ return returnData;
+ }
+
+ private static void loadKeyRings(Activity context, int type) {
+ Cursor cursor;
+ if (type == TYPE_SECRET) {
+ mSecretKeyRings.clear();
+ mSecretKeyIdToIdMap.clear();
+ mSecretKeyIdToKeyRingMap.clear();
+ cursor = context.managedQuery(SecretKeys.CONTENT_URI, SECRET_KEY_PROJECTION,
+ null, null, null);
+ } else {
+ mPublicKeyRings.clear();
+ mPublicKeyIdToIdMap.clear();
+ mPublicKeyIdToKeyRingMap.clear();
+ cursor = context.managedQuery(PublicKeys.CONTENT_URI, PUBLIC_KEY_PROJECTION,
+ null, null, null);
+ }
+
+ for (int i = 0; i < cursor.getCount(); ++i) {
+ cursor.moveToPosition(i);
+ String sharedIdColumn = PublicKeys._ID; // same in both
+ String sharedKeyIdColumn = PublicKeys.KEY_ID; // same in both
+ String sharedKeyDataColumn = PublicKeys.KEY_DATA; // same in both
+ int idIndex = cursor.getColumnIndex(sharedIdColumn);
+ int keyIdIndex = cursor.getColumnIndex(sharedKeyIdColumn);
+ int keyDataIndex = cursor.getColumnIndex(sharedKeyDataColumn);
+
+ byte keyData[] = cursor.getBlob(keyDataIndex);
+ int id = cursor.getInt(idIndex);
+ long keyId = cursor.getLong(keyIdIndex);
+
+ try {
+ if (type == TYPE_SECRET) {
+ PGPSecretKeyRing key = new PGPSecretKeyRing(keyData);
+ mSecretKeyRings.add(key);
+ mSecretKeyIdToIdMap.put(keyId, id);
+ mSecretKeyIdToKeyRingMap.put(keyId, key);
+ } else {
+ PGPPublicKeyRing key = new PGPPublicKeyRing(keyData);
+ mPublicKeyRings.add(key);
+ mPublicKeyIdToIdMap.put(keyId, id);
+ mPublicKeyIdToKeyRingMap.put(keyId, key);
+ }
+ } catch (IOException e) {
+ // TODO: some error handling
+ } catch (PGPException e) {
+ // TODO: some error handling
+ }
+ }
+
+ if (type == TYPE_SECRET) {
+ Collections.sort(mSecretKeyRings, new SecretKeySorter());
+ } else {
+ Collections.sort(mPublicKeyRings, new PublicKeySorter());
+ }
+ }
+
+ public static Date getCreationDate(PGPPublicKey key) {
+ return key.getCreationTime();
+ }
+
+ public static Date getCreationDate(PGPSecretKey key) {
+ return key.getPublicKey().getCreationTime();
+ }
+
+ public static PGPPublicKey getMasterKey(PGPPublicKeyRing keyRing) {
+ for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
+ if (key.isMasterKey()) {
+ return key;
+ }
+ }
+
+ return null;
+ }
+
+ public static PGPSecretKey getMasterKey(PGPSecretKeyRing keyRing) {
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ if (key.isMasterKey()) {
+ return key;
+ }
+ }
+
+ return null;
+ }
+
+ 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;
+ }
+
+ 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(long masterKeyId) {
+ PGPPublicKeyRing keyRing = mPublicKeyIdToKeyRingMap.get(masterKeyId);
+ if (keyRing == null) {
+ return null;
+ }
+ Vector<PGPPublicKey> encryptKeys = getUsableEncryptKeys(keyRing);
+ if (encryptKeys.size() == 0) {
+ return null;
+ }
+ return encryptKeys.get(0);
+ }
+
+ public static PGPSecretKey getSigningKey(long masterKeyId) {
+ PGPSecretKeyRing keyRing = mSecretKeyIdToKeyRingMap.get(masterKeyId);
+ if (keyRing == null) {
+ return null;
+ }
+ Vector<PGPSecretKey> signingKeys = getUsableSigningKeys(keyRing);
+ if (signingKeys.size() == 0) {
+ return null;
+ }
+ return signingKeys.get(0);
+ }
+
+ public static String getMainUserId(PGPPublicKey key) {
+ for (String userId : new IterableIterator<String>(key.getUserIDs())) {
+ return userId;
+ }
+ return null;
+ }
+
+ 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 = context.getResources().getString(R.string.unknown_user_id);
+ }
+ return userId;
+ }
+
+ public static String getMainUserIdSafe(Context context, PGPSecretKey key) {
+ String userId = getMainUserId(key);
+ if (userId == null) {
+ userId = context.getResources().getString(R.string.unknown_user_id);
+ }
+ return userId;
+ }
+
+ public static PGPPublicKeyRing getPublicKeyRing(long keyId) {
+ return mPublicKeyIdToKeyRingMap.get(keyId);
+ }
+
+ public static PGPSecretKeyRing getSecretKeyRing(long keyId) {
+ return mSecretKeyIdToKeyRingMap.get(keyId);
+ }
+
+ public static boolean isEncryptionKey(PGPPublicKey key) {
+ if (!key.isEncryptionKey()) {
+ return false;
+ }
+
+ if (key.getVersion() <= 3) {
+ // this must be true now
+ return key.isEncryptionKey();
+ }
+
+ // special case, this algorithm, no need to look further
+ if (key.getAlgorithm() == PGPPublicKey.ELGAMAL_ENCRYPT) {
+ return true;
+ }
+
+ for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
+ if (!key.isMasterKey() || sig.getKeyID() == key.getKeyID()) {
+ PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
+
+ if ((hashed.getKeyFlags() &
+ (KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE)) != 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public static boolean isEncryptionKey(PGPSecretKey key) {
+ return isEncryptionKey(key.getPublicKey());
+ }
+
+ public static boolean isSigningKey(PGPPublicKey key) {
+ if (key.getVersion() <= 3) {
+ return true;
+ }
+
+ for (PGPSignature sig : new IterableIterator<PGPSignature>(key.getSignatures())) {
+ if (!key.isMasterKey() || sig.getKeyID() == key.getKeyID()) {
+ PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets();
+
+ if ((hashed.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) {
+ String algorithmStr = null;
+
+ switch (key.getAlgorithm()) {
+ 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 + ", " + key.getBitStrength() + "bit";
+ }
+
+ public static String getAlgorithmInfo(PGPSecretKey key) {
+ return getAlgorithmInfo(key.getPublicKey());
+ }
+
+ public static void deleteKey(Activity context, PGPPublicKeyRing keyRing) {
+ PGPPublicKey masterKey = getMasterKey(keyRing);
+ Uri uri = Uri.withAppendedPath(PublicKeys.CONTENT_URI_BY_KEY_ID, "" + masterKey.getKeyID());
+ context.getContentResolver().delete(uri, null, null);
+ loadKeyRings(context, TYPE_PUBLIC);
+ }
+
+ public static void deleteKey(Activity context, PGPSecretKeyRing keyRing) {
+ PGPSecretKey masterKey = getMasterKey(keyRing);
+ Uri uri = Uri.withAppendedPath(SecretKeys.CONTENT_URI_BY_KEY_ID, "" + masterKey.getKeyID());
+ context.getContentResolver().delete(uri, null, null);
+ loadKeyRings(context, TYPE_SECRET);
+ }
+
+ public static PGPPublicKey findPublicKey(long keyId) {
+ PGPPublicKey key = null;
+ for (int i = 0; i < mPublicKeyRings.size(); ++i) {
+ PGPPublicKeyRing keyRing = mPublicKeyRings.get(i);
+ try {
+ key = keyRing.getPublicKey(keyId);
+ if (key != null) {
+ return key;
+ }
+ } catch (PGPException e) {
+ // just not found, can ignore this
+ }
+ }
+ return null;
+ }
+
+ public static PGPSecretKey findSecretKey(long keyId) {
+ PGPSecretKey key = null;
+ for (int i = 0; i < mSecretKeyRings.size(); ++i) {
+ PGPSecretKeyRing keyRing = mSecretKeyRings.get(i);
+ key = keyRing.getSecretKey(keyId);
+ if (key != null) {
+ return key;
+ }
+ }
+ return null;
+ }
+
+ public static PGPSecretKeyRing findSecretKeyRing(long keyId) {
+ for (int i = 0; i < mSecretKeyRings.size(); ++i) {
+ PGPSecretKeyRing keyRing = mSecretKeyRings.get(i);
+ PGPSecretKey key = null;
+ key = keyRing.getSecretKey(keyId);
+ if (key != null) {
+ return keyRing;
+ }
+ }
+ return null;
+ }
+
+ public static PGPPublicKeyRing findPublicKeyRing(long keyId) {
+ for (int i = 0; i < mPublicKeyRings.size(); ++i) {
+ PGPPublicKeyRing keyRing = mPublicKeyRings.get(i);
+ PGPPublicKey key = null;
+ try {
+ key = keyRing.getPublicKey(keyId);
+ if (key != null) {
+ return keyRing;
+ }
+ } catch (PGPException e) {
+ // key not found
+ }
+ }
+ return null;
+ }
+
+ public static PGPPublicKey getPublicMasterKey(long keyId) {
+ PGPPublicKey key = null;
+ for (int i = 0; i < mPublicKeyRings.size(); ++i) {
+ PGPPublicKeyRing keyRing = mPublicKeyRings.get(i);
+ try {
+ key = keyRing.getPublicKey(keyId);
+ if (key != null) {
+ return getMasterKey(keyRing);
+ }
+ } catch (PGPException e) {
+ // just not found, can ignore this
+ }
+ }
+ return null;
+ }
+
+ public static void encrypt(InputStream inStream, OutputStream outStream,
+ long encryptionKeyIds[], long signatureKeyId,
+ String signaturePassPhrase,
+ ProgressDialogUpdater progress)
+ throws IOException, GeneralException, PGPException, NoSuchProviderException,
+ NoSuchAlgorithmException, SignatureException {
+ Security.addProvider(new BouncyCastleProvider());
+
+ ArmoredOutputStream armorOut = new ArmoredOutputStream(outStream);
+ armorOut.setHeader("Version", FULL_VERSION);
+ OutputStream out = armorOut;
+ OutputStream encryptOut = null;
+
+ PGPSecretKey signingKey = null;
+ PGPSecretKeyRing signingKeyRing = null;
+ PGPPrivateKey signaturePrivateKey = null;
+
+ if (encryptionKeyIds == null || encryptionKeyIds.length == 0) {
+ throw new GeneralException("no encryption key(s) given");
+ }
+
+ if (signatureKeyId != 0) {
+ signingKeyRing = findSecretKeyRing(signatureKeyId);
+ signingKey = getSigningKey(signatureKeyId);
+ if (signingKey == null) {
+ throw new GeneralException("signature failed");
+ }
+
+ if (signaturePassPhrase == null) {
+ throw new GeneralException("no pass phrase given");
+ }
+ signaturePrivateKey = signingKey.extractPrivateKey(signaturePassPhrase.toCharArray(),
+ new BouncyCastleProvider());
+ }
+
+ PGPSignatureGenerator signatureGenerator = null;
+ progress.setProgress("preparing data...", 0, 100);
+
+ ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+ int n = 0;
+ byte[] buffer = new byte[1 << 16];
+ while ((n = inStream.read(buffer)) > 0) {
+ byteOut.write(buffer, 0, n);
+ }
+ byteOut.close();
+ byte messageData[] = byteOut.toByteArray();
+
+ progress.setProgress("preparing streams...", 20, 100);
+ // encryptFile and compress input file content
+ PGPEncryptedDataGenerator cPk =
+ new PGPEncryptedDataGenerator(PGPEncryptedData.AES_256, true, new SecureRandom(),
+ new BouncyCastleProvider());
+
+ for (int i = 0; i < encryptionKeyIds.length; ++i) {
+ PGPPublicKey key = getEncryptPublicKey(encryptionKeyIds[i]);
+ if (key != null) {
+ cPk.addMethod(key);
+ }
+ }
+ encryptOut = cPk.open(out, new byte[1 << 16]);
+
+ if (signatureKeyId != 0) {
+ progress.setProgress("preparing signature...", 30, 100);
+ signatureGenerator =
+ new PGPSignatureGenerator(signingKey.getPublicKey().getAlgorithm(),
+ HashAlgorithmTags.SHA1,
+ new BouncyCastleProvider());
+ signatureGenerator.initSign(PGPSignature.BINARY_DOCUMENT, signaturePrivateKey);
+ String userId = getMainUserId(getMasterKey(signingKeyRing));
+
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+ spGen.setSignerUserID(false, userId);
+ signatureGenerator.setHashedSubpackets(spGen.generate());
+ }
+
+ PGPCompressedDataGenerator compressGen =
+ new PGPCompressedDataGenerator(PGPCompressedDataGenerator.ZLIB);
+ BCPGOutputStream bcpgOut = new BCPGOutputStream(compressGen.open(encryptOut));
+ if (signatureKeyId != 0) {
+ signatureGenerator.generateOnePassVersion(false).encode(bcpgOut);
+ }
+
+ PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator();
+ // file name not needed, so empty string
+ OutputStream pOut = literalGen.open(bcpgOut, PGPLiteralData.BINARY, "",
+ messageData.length, new Date());
+
+ progress.setProgress("encrypting...", 40, 100);
+ pOut.write(messageData);
+
+ if (signatureKeyId != 0) {
+ progress.setProgress("finishing signature...", 70, 100);
+ signatureGenerator.update(messageData);
+ }
+
+ literalGen.close();
+
+ if (signatureKeyId != 0) {
+ signatureGenerator.generate().encode(pOut);
+ }
+ compressGen.close();
+ encryptOut.close();
+ out.close();
+
+ progress.setProgress("done.", 100, 100);
+ }
+
+ public static void sign(InputStream inStream, OutputStream outStream,
+ long signatureKeyId, String signaturePassPhrase,
+ ProgressDialogUpdater progress)
+ throws GeneralException, PGPException, IOException, NoSuchAlgorithmException,
+ SignatureException {
+ Security.addProvider(new BouncyCastleProvider());
+
+ ArmoredOutputStream armorOut = new ArmoredOutputStream(outStream);
+ armorOut.setHeader("Version", FULL_VERSION);
+ OutputStream out = armorOut;
+ OutputStream signOut = out;
+
+ PGPSecretKey signingKey = null;
+ PGPSecretKeyRing signingKeyRing = null;
+ PGPPrivateKey signaturePrivateKey = null;
+
+ if (signatureKeyId == 0) {
+ throw new GeneralException("no signature key given");
+ }
+
+ signingKeyRing = findSecretKeyRing(signatureKeyId);
+ signingKey = getSigningKey(signatureKeyId);
+ if (signingKey == null) {
+ throw new GeneralException("signature failed");
+ }
+
+ if (signaturePassPhrase == null) {
+ throw new GeneralException("no pass phrase given");
+ }
+ signaturePrivateKey =
+ signingKey.extractPrivateKey(signaturePassPhrase.toCharArray(),
+ new BouncyCastleProvider());
+
+ PGPSignatureGenerator signatureGenerator = null;
+ progress.setProgress("preparing data...", 0, 100);
+
+ progress.setProgress("preparing signature...", 30, 100);
+ signatureGenerator =
+ new PGPSignatureGenerator(signingKey.getPublicKey().getAlgorithm(),
+ HashAlgorithmTags.SHA1,
+ new BouncyCastleProvider());
+ signatureGenerator.initSign(PGPSignature.CANONICAL_TEXT_DOCUMENT, signaturePrivateKey);
+ String userId = getMainUserId(getMasterKey(signingKeyRing));
+
+ PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
+ spGen.setSignerUserID(false, userId);
+ signatureGenerator.setHashedSubpackets(spGen.generate());
+
+ progress.setProgress("signing...", 40, 100);
+ int n = 0;
+ byte[] buffer = new byte[1 << 16];
+ while ((n = inStream.read(buffer)) > 0) {
+ signatureGenerator.update(buffer, 0, n);
+ }
+
+ signatureGenerator.generate().encode(signOut);
+ signOut.close();
+ out.close();
+
+ progress.setProgress("done.", 100, 100);
+ }
+
+ public static long getDecryptionKeyId(InputStream inStream)
+ throws GeneralException, IOException {
+ InputStream in = PGPUtil.getDecoderStream(inStream);
+ 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 GeneralException("data not valid encryption data");
+ }
+
+ // find the secret key
+ PGPSecretKey secretKey = null;
+ for (PGPPublicKeyEncryptedData pbe :
+ new IterableIterator<PGPPublicKeyEncryptedData>(enc.getEncryptedDataObjects())) {
+ secretKey = findSecretKey(pbe.getKeyID());
+ if (secretKey != null) {
+ break;
+ }
+ }
+ if (secretKey == null) {
+ throw new GeneralException("couldn't find a secret key to decrypt");
+ }
+
+ return secretKey.getKeyID();
+ }
+
+ public static Bundle decrypt(InputStream inStream, OutputStream outStream,
+ String passPhrase, ProgressDialogUpdater progress)
+ throws IOException, GeneralException, PGPException, SignatureException {
+ Bundle returnData = new Bundle();
+ InputStream in = PGPUtil.getDecoderStream(inStream);
+ PGPObjectFactory pgpF = new PGPObjectFactory(in);
+ PGPEncryptedDataList enc;
+ Object o = pgpF.nextObject();
+ long signatureKeyId = 0;
+
+ progress.setProgress("reading data...", 0, 100);
+
+ if (o instanceof PGPEncryptedDataList) {
+ enc = (PGPEncryptedDataList) o;
+ } else {
+ enc = (PGPEncryptedDataList) pgpF.nextObject();
+ }
+
+ if (enc == null) {
+ throw new GeneralException("data not valid encryption data");
+ }
+
+ progress.setProgress("finding key...", 10, 100);
+ // find the secret key
+ PGPPublicKeyEncryptedData pbe = null;
+ PGPSecretKey secretKey = null;
+ for (PGPPublicKeyEncryptedData encData :
+ new IterableIterator<PGPPublicKeyEncryptedData>(enc.getEncryptedDataObjects())) {
+ secretKey = findSecretKey(encData.getKeyID());
+ if (secretKey != null) {
+ pbe = encData;
+ break;
+ }
+ }
+ if (secretKey == null) {
+ throw new GeneralException("couldn't find a secret key to decrypt");
+ }
+
+ progress.setProgress("extracting key...", 20, 100);
+ PGPPrivateKey privateKey = null;
+ try {
+ privateKey = secretKey.extractPrivateKey(passPhrase.toCharArray(),
+ new BouncyCastleProvider());
+ } catch (PGPException e) {
+ throw new PGPException("wrong pass phrase");
+ }
+ progress.setProgress("decrypting data...", 30, 100);
+ InputStream clear = pbe.getDataStream(privateKey, new BouncyCastleProvider());
+ PGPObjectFactory plainFact = new PGPObjectFactory(clear);
+ Object dataChunk = plainFact.nextObject();
+ PGPOnePassSignature signature = null;
+ PGPPublicKey signatureKey = null;
+ int signatureIndex = -1;
+
+ if (dataChunk instanceof PGPCompressedData) {
+ progress.setProgress("decompressing data...", 50, 100);
+ PGPObjectFactory fact =
+ new PGPObjectFactory(((PGPCompressedData) dataChunk).getDataStream());
+ dataChunk = fact.nextObject();
+ plainFact = fact;
+ }
+
+ if (dataChunk instanceof PGPOnePassSignatureList) {
+ progress.setProgress("processing signature...", 60, 100);
+ returnData.putBoolean("signature", true);
+ PGPOnePassSignatureList sigList = (PGPOnePassSignatureList) dataChunk;
+ for (int i = 0; i < sigList.size(); ++i) {
+ signature = sigList.get(i);
+ signatureKey = findPublicKey(signature.getKeyID());
+ if (signatureKeyId == 0) {
+ signatureKeyId = signature.getKeyID();
+ }
+ if (signatureKey == null) {
+ signature = null;
+ } else {
+ signatureIndex = i;
+ signatureKeyId = signature.getKeyID();
+ String userId = null;
+ PGPPublicKeyRing sigKeyRing = findPublicKeyRing(signatureKeyId);
+ if (sigKeyRing != null) {
+ userId = getMainUserId(getMasterKey(sigKeyRing));
+ }
+ returnData.putString("signatureUserId", userId);
+ break;
+ }
+ }
+
+ returnData.putLong("signatureKeyId", signatureKeyId);
+
+ if (signature != null) {
+ signature.initVerify(signatureKey, new BouncyCastleProvider());
+ } else {
+ returnData.putBoolean("signatureUnknown", true);
+ }
+
+ dataChunk = plainFact.nextObject();
+ }
+
+ if (dataChunk instanceof PGPLiteralData) {
+ progress.setProgress("unpacking data...", 70, 100);
+ PGPLiteralData literalData = (PGPLiteralData) dataChunk;
+ BufferedOutputStream out = new BufferedOutputStream(outStream);
+
+ byte[] buffer = new byte[1 << 16];
+ InputStream dataIn = literalData.getInputStream();
+
+ int bytesRead = 0;
+ while ((bytesRead = dataIn.read(buffer)) > 0) {
+ out.write(buffer, 0, bytesRead);
+ if (signature != null) {
+ try {
+ signature.update(buffer, 0, bytesRead);
+ } catch (SignatureException e) {
+ returnData.putBoolean("signatureSuccess", false);
+ signature = null;
+ }
+ }
+ }
+
+ out.close();
+
+ if (signature != null) {
+ progress.setProgress("verifying signature...", 80, 100);
+ PGPSignatureList signatureList = (PGPSignatureList) plainFact.nextObject();
+ PGPSignature messageSignature = (PGPSignature) signatureList.get(signatureIndex);
+ if (signature.verify(messageSignature)) {
+ returnData.putBoolean("signatureSuccess", true);
+ } else {
+ returnData.putBoolean("signatureSuccess", false);
+ }
+ }
+ }
+
+ // TODO: add integrity somewhere
+ if (pbe.isIntegrityProtected()) {
+ progress.setProgress("verifying integrity...", 90, 100);
+ if (!pbe.verify()) {
+ System.err.println("message failed integrity check");
+ } else {
+ System.err.println("message integrity check passed");
+ }
+ } else {
+ System.err.println("no message integrity check");
+ }
+
+ progress.setProgress("done.", 100, 100);
+ return returnData;
+ }
+
+ public static Vector<PGPPublicKeyRing> getPublicKeyRings() {
+ return mPublicKeyRings;
+ }
+
+ public static Vector<PGPSecretKeyRing> getSecretKeyRings() {
+ return mSecretKeyRings;
+ }
+}
diff --git a/src/org/thialfihar/android/apg/AskForSecretKeyPassPhrase.java b/src/org/thialfihar/android/apg/AskForSecretKeyPassPhrase.java
new file mode 100644
index 000000000..ed1c16833
--- /dev/null
+++ b/src/org/thialfihar/android/apg/AskForSecretKeyPassPhrase.java
@@ -0,0 +1,118 @@
+/*
+ * 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.thialfihar.android.apg;
+
+import org.bouncycastle2.jce.provider.BouncyCastleProvider;
+import org.bouncycastle2.openpgp.PGPException;
+import org.bouncycastle2.openpgp.PGPSecretKey;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.text.InputType;
+import android.text.method.PasswordTransformationMethod;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnKeyListener;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+public class AskForSecretKeyPassPhrase {
+ public static final int DIALOG_PASS_PHRASE = 12345;
+
+ public static interface PassPhraseCallbackInterface {
+ void passPhraseCallback(String passPhrase);
+ }
+
+ public static Dialog createDialog(Activity context, long secretKeyId,
+ PassPhraseCallbackInterface callback) {
+ AlertDialog.Builder alert = new AlertDialog.Builder(context);
+
+ final PGPSecretKey secretKey =
+ Apg.getMasterKey(Apg.findSecretKeyRing(secretKeyId));
+ if (secretKey == null) {
+ return null;
+ }
+
+ String userId = Apg.getMainUserIdSafe(context, secretKey);
+
+ alert.setTitle(R.string.title_authentification);
+ alert.setMessage("Pass phrase for " + userId);
+
+ final EditText input = new EditText(context);
+ input.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ input.setTransformationMethod(new PasswordTransformationMethod());
+ input.setOnKeyListener(new OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // If the event is a key-down event on the "enter" button
+ if (event.getAction() == KeyEvent.ACTION_DOWN &&
+ keyCode == KeyEvent.KEYCODE_ENTER) {
+ try {
+ ((AlertDialog) v.getParent()).getButton(AlertDialog.BUTTON_POSITIVE)
+ .performClick();
+ } catch (ClassCastException e) {
+ // don't do anything if we're not in that dialog
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+ // 5dip padding
+ int padding = (int) (10 * context.getResources().getDisplayMetrics().densityDpi / 160);
+ LinearLayout layout = new LinearLayout(context);
+ layout.setPadding(padding, 0, padding, 0);
+ layout.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
+ LayoutParams.WRAP_CONTENT));
+ input.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
+ LayoutParams.WRAP_CONTENT));
+ layout.addView(input);
+ alert.setView(layout);
+
+ final PassPhraseCallbackInterface cb = callback;
+ final Activity activity = context;
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ activity.removeDialog(DIALOG_PASS_PHRASE);
+ String passPhrase = "" + input.getText();
+ try {
+ secretKey.extractPrivateKey(passPhrase.toCharArray(),
+ new BouncyCastleProvider());
+ } catch (PGPException e) {
+ Toast.makeText(activity,
+ R.string.wrong_pass_phrase,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ cb.passPhraseCallback(passPhrase);
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ activity.removeDialog(DIALOG_PASS_PHRASE);
+ }
+ });
+
+ return alert.create();
+ }
+}
diff --git a/src/org/thialfihar/android/apg/DecryptMessageActivity.java b/src/org/thialfihar/android/apg/DecryptMessageActivity.java
new file mode 100644
index 000000000..179d5be55
--- /dev/null
+++ b/src/org/thialfihar/android/apg/DecryptMessageActivity.java
@@ -0,0 +1,343 @@
+/*
+ * 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.thialfihar.android.apg;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.Security;
+import java.security.SignatureException;
+import java.util.regex.Matcher;
+
+import org.bouncycastle2.jce.provider.BouncyCastleProvider;
+import org.bouncycastle2.openpgp.PGPException;
+import org.bouncycastle2.util.Strings;
+
+import android.app.Activity;
+import android.app.Dialog;
+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.text.ClipboardManager;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class DecryptMessageActivity extends Activity
+ implements Runnable, ProgressDialogUpdater,
+ AskForSecretKeyPassPhrase.PassPhraseCallbackInterface {
+ static final int GET_PUCLIC_KEYS = 1;
+ static final int GET_SECRET_KEY = 2;
+
+ static final int DIALOG_DECRYPTING = 1;
+
+ static final int MESSAGE_PROGRESS_UPDATE = 1;
+ static final int MESSAGE_DONE = 2;
+
+ private long mDecryptionKeyId = 0;
+ private long mSignatureKeyId = 0;
+
+ private String mReplyTo = null;
+ private String mSubject = null;
+
+ private ProgressDialog mProgressDialog = null;
+ private Thread mRunningThread = null;
+
+ private EditText mMessage = null;
+ private LinearLayout mSignatureLayout = null;
+ private ImageView mSignatureStatusImage = null;
+ private TextView mUserId = null;
+ private TextView mUserIdRest = null;
+ private Button mDecryptButton = null;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ Bundle data = msg.getData();
+ if (data != null) {
+ int type = data.getInt("type");
+ switch (type) {
+ case MESSAGE_PROGRESS_UPDATE: {
+ String message = data.getString("message");
+ if (mProgressDialog != null) {
+ if (message != null) {
+ mProgressDialog.setMessage(message);
+ }
+ mProgressDialog.setMax(data.getInt("max"));
+ mProgressDialog.setProgress(data.getInt("progress"));
+ }
+ break;
+ }
+
+ case MESSAGE_DONE: {
+ removeDialog(DIALOG_DECRYPTING);
+ mProgressDialog = null;
+ mSignatureKeyId = 0;
+ String error = data.getString("error");
+ String decryptedMessage = data.getString("decryptedMessage");
+ if (error != null) {
+ Toast.makeText(DecryptMessageActivity.this,
+ "Error: " + data.getString("error"),
+ Toast.LENGTH_SHORT).show();
+ }
+ mSignatureLayout.setVisibility(View.INVISIBLE);
+ if (decryptedMessage != null) {
+ mMessage.setText(decryptedMessage);
+ mDecryptButton.setText(R.string.btn_reply);
+ mDecryptButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ replyClicked();
+ }
+ });
+
+ if (data.getBoolean("signature")) {
+ String userId = data.getString("signatureUserId");
+ mSignatureKeyId = data.getLong("signatureKeyId");
+ mUserIdRest.setText("id: " +
+ Long.toHexString(mSignatureKeyId & 0xffffffffL));
+ if (userId == null) {
+ userId =
+ getResources()
+ .getString(
+ R.string.unknown_user_id);
+ }
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mUserIdRest.setText("<" + chunks[1]);
+ }
+ mUserId.setText(userId);
+
+ if (data.getBoolean("signatureSuccess")) {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_ok);
+ } else if (data.getBoolean("signatureUnknown")) {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ } else {
+ mSignatureStatusImage.setImageResource(R.drawable.overlay_error);
+ }
+ mSignatureLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.decrypt_message);
+
+ Apg.initialize(this);
+
+ mMessage = (EditText) findViewById(R.id.message);
+ mDecryptButton = (Button) findViewById(R.id.btn_decrypt);
+ mSignatureLayout = (LinearLayout) findViewById(R.id.layout_signature);
+ mSignatureStatusImage = (ImageView) findViewById(R.id.ic_signature_status);
+ mUserId = (TextView) findViewById(R.id.main_user_id);
+ mUserIdRest = (TextView) findViewById(R.id.main_user_id_rest);
+
+ Intent intent = getIntent();
+ if (intent.getAction() != null && intent.getAction().equals(Intent.ACTION_VIEW)) {
+ Uri uri = intent.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 = Strings.fromUTF8ByteArray(byteOut.toByteArray());
+ mMessage.setText(data);
+ } catch (FileNotFoundException e) {
+ // ignore, then
+ } catch (IOException e) {
+ // ignore, then
+ }
+ } else if (intent.getAction() != null && intent.getAction().equals(Apg.Intent.DECRYPT)) {
+ String data = intent.getExtras().getString("data");
+ if (data != null) {
+ Matcher matcher = Apg.PGP_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ mMessage.setText(data);
+ }
+ }
+ mReplyTo = intent.getExtras().getString("replyTo");
+ mSubject = intent.getExtras().getString("subject");
+ } else {
+ ClipboardManager clip = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ String data = "";
+ Matcher matcher = Apg.PGP_MESSAGE.matcher(clip.getText());
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ mMessage.setText(data);
+ Toast.makeText(this, R.string.using_clipboard_content, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ mSignatureLayout.setVisibility(View.INVISIBLE);
+
+ mDecryptButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ decryptClicked();
+ }
+ });
+
+ if (mMessage.getText().length() > 0) {
+ mDecryptButton.performClick();
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case DIALOG_DECRYPTING: {
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setMessage("initializing...");
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+ }
+
+ case AskForSecretKeyPassPhrase.DIALOG_PASS_PHRASE: {
+ return AskForSecretKeyPassPhrase.createDialog(this, mDecryptionKeyId, this);
+ }
+ }
+
+ return super.onCreateDialog(id);
+ }
+
+ @Override
+ public void setProgress(int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt("type", MESSAGE_PROGRESS_UPDATE);
+ data.putInt("progress", progress);
+ data.putInt("max", max);
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ @Override
+ public void setProgress(String message, int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt("type", MESSAGE_PROGRESS_UPDATE);
+ data.putString("message", message);
+ data.putInt("progress", progress);
+ data.putInt("max", max);
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ private void decryptClicked() {
+ String error = null;
+ ByteArrayInputStream in =
+ new ByteArrayInputStream(mMessage.getText().toString().getBytes());
+ try {
+ mDecryptionKeyId = Apg.getDecryptionKeyId(in);
+ showDialog(AskForSecretKeyPassPhrase.DIALOG_PASS_PHRASE);
+ } catch (IOException e) {
+ error = e.getLocalizedMessage();
+ } catch (Apg.GeneralException e) {
+ error = e.getLocalizedMessage();
+ }
+ if (error != null) {
+ Toast.makeText(this, "Error: " + error, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void replyClicked() {
+ Intent intent = new Intent(this, EncryptMessageActivity.class);
+ intent.setAction(Apg.Intent.ENCRYPT);
+ String data = mMessage.getText().toString();
+ data = data.replaceAll("(?m)^", "> ");
+ data = "\n\n" + data;
+ intent.putExtra("data", data);
+ intent.putExtra("subject", "Re: " + mSubject);
+ intent.putExtra("sendTo", mReplyTo);
+ intent.putExtra("eyId", mSignatureKeyId);
+ intent.putExtra("signatureKeyId", mDecryptionKeyId);
+ intent.putExtra("encryptionKeyIds", new long[] { mSignatureKeyId });
+ startActivity(intent);
+ }
+
+ public void passPhraseCallback(String passPhrase) {
+ Apg.setPassPhrase(passPhrase);
+ decryptStart();
+ }
+
+ private void decryptStart() {
+ showDialog(DIALOG_DECRYPTING);
+ mRunningThread = new Thread(this);
+ mRunningThread.start();
+ }
+
+ public void run() {
+ String error = null;
+ Security.addProvider(new BouncyCastleProvider());
+
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ ByteArrayInputStream in =
+ new ByteArrayInputStream(mMessage.getText().toString().getBytes());
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ try {
+ data = Apg.decrypt(in, out, Apg.getPassPhrase(), this);
+ } catch (PGPException e) {
+ error = e.getMessage();
+ } catch (IOException e) {
+ error = e.getMessage();
+ } catch (SignatureException e) {
+ error = e.getMessage();
+ e.printStackTrace();
+ } catch (Apg.GeneralException e) {
+ error = e.getMessage();
+ }
+
+ data.putInt("type", MESSAGE_DONE);
+
+ if (error != null) {
+ data.putString("error", error);
+ } else {
+ data.putString("decryptedMessage", Strings.fromUTF8ByteArray(out.toByteArray()));
+ }
+
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+}
diff --git a/src/org/thialfihar/android/apg/EditKeyActivity.java b/src/org/thialfihar/android/apg/EditKeyActivity.java
new file mode 100644
index 000000000..b5b7045b7
--- /dev/null
+++ b/src/org/thialfihar/android/apg/EditKeyActivity.java
@@ -0,0 +1,401 @@
+/*
+ * 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.thialfihar.android.apg;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.util.Vector;
+
+import org.bouncycastle2.openpgp.PGPException;
+import org.bouncycastle2.openpgp.PGPSecretKey;
+import org.bouncycastle2.openpgp.PGPSecretKeyRing;
+import org.thialfihar.android.apg.ui.widget.SectionView;
+import org.thialfihar.android.apg.utils.IterableIterator;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.InputType;
+import android.text.method.PasswordTransformationMethod;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+public class EditKeyActivity extends Activity
+ implements OnClickListener, ProgressDialogUpdater, Runnable {
+ static final int OPTION_MENU_NEW_PASS_PHRASE = 1;
+
+ static final int DIALOG_NEW_PASS_PHRASE = 1;
+ static final int DIALOG_PASS_PHRASES_DO_NOT_MATCH = 2;
+ static final int DIALOG_NO_PASS_PHRASE = 3;
+ static final int DIALOG_SAVING = 4;
+
+ static final int MESSAGE_PROGRESS_UPDATE = 1;
+ static final int MESSAGE_DONE = 2;
+
+ private PGPSecretKeyRing mKeyRing = null;
+
+ private SectionView mUserIds;
+ private SectionView mKeys;
+
+ private Button mSaveButton;
+ private Button mDiscardButton;
+
+ private ProgressDialog mProgressDialog = null;
+ private Thread mRunningThread = null;
+
+ private String mNewPassPhrase = null;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ Bundle data = msg.getData();
+ if (data != null) {
+ int type = data.getInt("type");
+ switch (type) {
+ case MESSAGE_PROGRESS_UPDATE: {
+ String message = data.getString("message");
+ if (mProgressDialog != null) {
+ if (message != null) {
+ mProgressDialog.setMessage(message);
+ }
+ mProgressDialog.setMax(data.getInt("max"));
+ mProgressDialog.setProgress(data.getInt("progress"));
+ }
+ break;
+ }
+
+ case MESSAGE_DONE: {
+ removeDialog(DIALOG_SAVING);
+ mProgressDialog = null;
+
+ String error = data.getString("error");
+ if (error != null) {
+ Toast.makeText(EditKeyActivity.this,
+ "Error: " + data.getString("error"),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(EditKeyActivity.this, R.string.key_saved,
+ Toast.LENGTH_SHORT).show();
+ setResult(RESULT_OK);
+ finish();
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.edit_key);
+
+ Vector<String> userIds = new Vector<String>();
+ Vector<PGPSecretKey> keys = new Vector<PGPSecretKey>();
+
+ Intent intent = getIntent();
+ long keyId = 0;
+ if (intent.getExtras() != null) {
+ keyId = intent.getExtras().getLong("keyId");
+ }
+ if (keyId != 0) {
+ PGPSecretKey masterKey = null;
+ mKeyRing = Apg.getSecretKeyRing(keyId);
+ if (mKeyRing != null) {
+ masterKey = Apg.getMasterKey(mKeyRing);
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(mKeyRing.getSecretKeys())) {
+ keys.add(key);
+ }
+ }
+ if (masterKey != null) {
+ for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
+ userIds.add(userId);
+ }
+ }
+ }
+
+ if (Apg.getPassPhrase() == null) {
+ Apg.setPassPhrase("");
+ }
+
+ mSaveButton = (Button) findViewById(R.id.btn_save);
+ mDiscardButton = (Button) findViewById(R.id.btn_discard);
+
+ mSaveButton.setOnClickListener(this);
+ mDiscardButton.setOnClickListener(this);
+
+ LayoutInflater inflater =
+ (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ LinearLayout container = (LinearLayout) findViewById(R.id.container);
+ mUserIds = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
+ mUserIds.setType(SectionView.TYPE_USER_ID);
+ mUserIds.setUserIds(userIds);
+ container.addView(mUserIds);
+ mKeys = (SectionView) inflater.inflate(R.layout.edit_key_section, container, false);
+ mKeys.setType(SectionView.TYPE_KEY);
+ mKeys.setKeys(keys);
+ container.addView(mKeys);
+
+ Toast.makeText(this, "Warning: Key editing is still kind of beta.", Toast.LENGTH_LONG).show();
+ }
+
+ public boolean havePassPhrase() {
+ return (Apg.getPassPhrase() != null && !Apg.getPassPhrase().equals("")) ||
+ (mNewPassPhrase != null && mNewPassPhrase.equals(""));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, OPTION_MENU_NEW_PASS_PHRASE, 0,
+ (havePassPhrase() ? "Change Pass Phrase" : "Set Pass Phrase"))
+ .setIcon(android.R.drawable.ic_menu_add);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case OPTION_MENU_NEW_PASS_PHRASE: {
+ showDialog(DIALOG_NEW_PASS_PHRASE);
+ return true;
+ }
+
+ default: {
+ break;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case DIALOG_SAVING: {
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setMessage("saving...");
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+ }
+
+ case DIALOG_NEW_PASS_PHRASE: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ if (havePassPhrase()) {
+ alert.setTitle("Change Pass Phrase");
+ } else {
+ alert.setTitle("Set Pass Phrase");
+ }
+ alert.setMessage("Enter the pass phrase twice.");
+
+ final EditText input1 = new EditText(this);
+ final EditText input2 = new EditText(this);
+ input1.setText("");
+ input2.setText("");
+ input1.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ input2.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ input1.setTransformationMethod(new PasswordTransformationMethod());
+ input2.setTransformationMethod(new PasswordTransformationMethod());
+
+ // 5dip padding
+ int padding = (int) (10 * getResources().getDisplayMetrics().densityDpi / 160);
+ LinearLayout layout = new LinearLayout(this);
+ layout.setOrientation(LinearLayout.VERTICAL);
+ layout.setPadding(padding, 0, padding, 0);
+ layout.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
+ LayoutParams.WRAP_CONTENT));
+ input1.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
+ LayoutParams.WRAP_CONTENT));
+ input2.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
+ LayoutParams.WRAP_CONTENT));
+ layout.addView(input1);
+ layout.addView(input2);
+
+ alert.setView(layout);
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(DIALOG_NEW_PASS_PHRASE);
+
+ String passPhrase1 = "" + input1.getText();
+ String passPhrase2 = "" + input2.getText();
+ if (!passPhrase1.equals(passPhrase2)) {
+ showDialog(DIALOG_PASS_PHRASES_DO_NOT_MATCH);
+ return;
+ }
+
+ if (passPhrase1.equals("")) {
+ showDialog(DIALOG_NO_PASS_PHRASE);
+ return;
+ }
+
+ mNewPassPhrase = passPhrase1;
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(DIALOG_NEW_PASS_PHRASE);
+ }
+ });
+
+ return alert.create();
+ }
+
+ case DIALOG_PASS_PHRASES_DO_NOT_MATCH: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle("Error");
+ alert.setMessage("The pass phrases didn't match.");
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(DIALOG_PASS_PHRASES_DO_NOT_MATCH);
+ }
+ });
+ alert.setCancelable(false);
+
+ return alert.create();
+ }
+
+ case DIALOG_NO_PASS_PHRASE: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setIcon(android.R.drawable.ic_dialog_alert);
+ alert.setTitle("Error");
+ alert.setMessage("Empty pass phrases are not supported.");
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(DIALOG_NO_PASS_PHRASE);
+ }
+ });
+ alert.setCancelable(false);
+
+ return alert.create();
+ }
+
+ default: {
+ break;
+ }
+ }
+ return super.onCreateDialog(id);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mSaveButton) {
+ // TODO: some warning
+ saveClicked();
+ } else if (v == mDiscardButton) {
+ finish();
+ }
+ }
+
+ private void saveClicked() {
+ if ((Apg.getPassPhrase() == null || Apg.getPassPhrase().equals("")) &&
+ (mNewPassPhrase == null || mNewPassPhrase.equals(""))) {
+ Toast.makeText(this, R.string.set_a_pass_phrase, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ showDialog(DIALOG_SAVING);
+ mRunningThread = new Thread(this);
+ mRunningThread.start();
+ }
+
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ try {
+ String oldPassPhrase = Apg.getPassPhrase();
+ String newPassPhrase = mNewPassPhrase;
+ if (newPassPhrase == null) {
+ newPassPhrase = oldPassPhrase;
+ }
+ Apg.buildSecretKey(this, mUserIds, mKeys, oldPassPhrase, newPassPhrase, this);
+ } catch (NoSuchProviderException e) {
+ error = e.getMessage();
+ } catch (NoSuchAlgorithmException e) {
+ error = e.getMessage();
+ } catch (PGPException e) {
+ error = e.getMessage();
+ } catch (SignatureException e) {
+ error = e.getMessage();
+ } catch (Apg.GeneralException e) {
+ error = e.getMessage();
+ }
+
+ data.putInt("type", MESSAGE_DONE);
+
+ if (error != null) {
+ data.putString("error", error);
+ }
+
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ public void setProgress(int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt("type", MESSAGE_PROGRESS_UPDATE);
+ data.putInt("progress", progress);
+ data.putInt("max", max);
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ public void setProgress(String message, int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt("type", MESSAGE_PROGRESS_UPDATE);
+ data.putString("message", message);
+ data.putInt("progress", progress);
+ data.putInt("max", max);
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+} \ No newline at end of file
diff --git a/src/org/thialfihar/android/apg/EncryptMessageActivity.java b/src/org/thialfihar/android/apg/EncryptMessageActivity.java
new file mode 100644
index 000000000..27e4c29be
--- /dev/null
+++ b/src/org/thialfihar/android/apg/EncryptMessageActivity.java
@@ -0,0 +1,428 @@
+/*
+ * 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.thialfihar.android.apg;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.util.Vector;
+
+import org.bouncycastle2.openpgp.PGPException;
+import org.bouncycastle2.openpgp.PGPPublicKey;
+import org.bouncycastle2.openpgp.PGPPublicKeyRing;
+import org.bouncycastle2.openpgp.PGPSecretKey;
+import org.bouncycastle2.openpgp.PGPSecretKeyRing;
+import org.bouncycastle2.util.Strings;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class EncryptMessageActivity extends Activity
+ implements Runnable, ProgressDialogUpdater,
+ AskForSecretKeyPassPhrase.PassPhraseCallbackInterface {
+ static final int GET_PUCLIC_KEYS = 1;
+ static final int GET_SECRET_KEY = 2;
+
+ static final int DIALOG_ENCRYPTING = 1;
+
+ static final int MESSAGE_PROGRESS_UPDATE = 1;
+ static final int MESSAGE_DONE = 2;
+
+ private String mSubject = null;
+ private String mSendTo = null;
+
+ private long mEncryptionKeyIds[] = null;
+ private long mSignatureKeyId = 0;
+
+ private ProgressDialog mProgressDialog = null;
+ private Thread mRunningThread = null;
+
+ private EditText mMessage = null;
+ private Button mSelectKeysButton = null;
+ private Button mSendButton = null;
+ private CheckBox mSign = null;
+ private TextView mMainUserId = null;
+ private TextView mMainUserIdRest = null;
+
+ private Handler mhandler = new Handler() {
+ @Override
+ public void handleMessage(Message mSg) {
+ Bundle data = mSg.getData();
+ if (data != null) {
+ int type = data.getInt("type");
+ switch (type) {
+ case MESSAGE_PROGRESS_UPDATE: {
+ String message = data.getString("message");
+ if (mProgressDialog != null) {
+ if (message != null) {
+ mProgressDialog.setMessage(message);
+ }
+ mProgressDialog.setMax(data.getInt("max"));
+ mProgressDialog.setProgress(data.getInt("progress"));
+ }
+ break;
+ }
+
+ case MESSAGE_DONE: {
+ removeDialog(DIALOG_ENCRYPTING);
+ mProgressDialog = null;
+
+ String error = data.getString("error");
+ if (error != null) {
+ Toast.makeText(EncryptMessageActivity.this,
+ "Error: " + data.getString("error"),
+ Toast.LENGTH_SHORT).show();
+ return;
+ } else {
+ String message = data.getString("message");
+ String signature = data.getString("signature");
+ Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
+ emailIntent.setType("text/plain; charset=utf-8");
+ emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, message);
+ if (signature != null) {
+ String fullText = "-----BEGIN PGP SIGNED MESSAGE-----\n" +
+ "Hash: SHA256\n" + "\n" +
+ message + "\n" + signature;
+ emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, fullText);
+ }
+ if (mSubject != null) {
+ emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,
+ mSubject);
+ }
+ if (mSendTo != null) {
+ emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL,
+ new String[] { mSendTo });
+ }
+ EncryptMessageActivity.this.
+ startActivity(Intent.createChooser(emailIntent, "Send mail..."));
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ @Override
+ public void setProgress(int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt("type", MESSAGE_PROGRESS_UPDATE);
+ data.putInt("progress", progress);
+ data.putInt("max", max);
+ msg.setData(data);
+ mhandler.sendMessage(msg);
+ }
+
+ @Override
+ public void setProgress(String message, int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt("type", MESSAGE_PROGRESS_UPDATE);
+ data.putString("message", message);
+ data.putInt("progress", progress);
+ data.putInt("max", max);
+ msg.setData(data);
+ mhandler.sendMessage(msg);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.encrypt_message);
+
+ Apg.initialize(this);
+
+ mMessage = (EditText) findViewById(R.id.message);
+ mSelectKeysButton = (Button) findViewById(R.id.btn_selectEncryptKeys);
+ mSendButton = (Button) findViewById(R.id.btn_send);
+ mSign = (CheckBox) findViewById(R.id.sign);
+ mMainUserId = (TextView) findViewById(R.id.main_user_id);
+ mMainUserIdRest = (TextView) findViewById(R.id.main_user_id_rest);
+
+ Intent intent = getIntent();
+ if (intent.getAction() != null &&
+ intent.getAction().equals(Apg.Intent.ENCRYPT)) {
+ String data = intent.getExtras().getString("data");
+ mSendTo = intent.getExtras().getString("sendTo");
+ mSubject = intent.getExtras().getString("subject");
+ long signatureKeyId = intent.getExtras().getLong("signatureKeyId");
+ long encryptionKeyIds[] = intent.getExtras().getLongArray("encryptionKeyIds");
+ if (signatureKeyId != 0) {
+ PGPSecretKeyRing keyRing = Apg.findSecretKeyRing(signatureKeyId);
+ PGPSecretKey masterKey = null;
+ if (keyRing != null) {
+ masterKey = Apg.getMasterKey(keyRing);
+ if (masterKey != null) {
+ Vector<PGPSecretKey> signKeys = Apg.getUsableSigningKeys(keyRing);
+ if (signKeys.size() > 0) {
+ mSignatureKeyId = masterKey.getKeyID();
+ }
+ }
+ }
+ }
+
+ if (encryptionKeyIds != null) {
+ Vector<Long> goodIds = new Vector<Long>();
+ for (int i = 0; i < encryptionKeyIds.length; ++i) {
+ PGPPublicKeyRing keyRing = Apg.findPublicKeyRing(encryptionKeyIds[i]);
+ PGPPublicKey masterKey = null;
+ if (keyRing == null) {
+ continue;
+ }
+ masterKey = Apg.getMasterKey(keyRing);
+ if (masterKey == null) {
+ continue;
+ }
+ Vector<PGPPublicKey> encryptKeys = Apg.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);
+ }
+ }
+ }
+ if (data != null) {
+ mMessage.setText(data);
+ }
+ }
+
+ mSendButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ sendClicked();
+ }
+ });
+
+ mSelectKeysButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ selectPublicKeys();
+ }
+ });
+
+ mSign.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ CheckBox checkBox = (CheckBox) v;
+ if (checkBox.isChecked()) {
+ selectSecretKey();
+ } else {
+ mSignatureKeyId = 0;
+ Apg.setPassPhrase(null);
+ updateView();
+ }
+ }
+ });
+
+ updateView();
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case DIALOG_ENCRYPTING: {
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setMessage("initializing...");
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+ }
+
+ case AskForSecretKeyPassPhrase.DIALOG_PASS_PHRASE: {
+ return AskForSecretKeyPassPhrase.createDialog(this, mSignatureKeyId, this);
+ }
+ }
+
+ return super.onCreateDialog(id);
+ }
+
+ private void sendClicked() {
+ if (mSignatureKeyId != 0 && Apg.getPassPhrase() == null) {
+ showDialog(AskForSecretKeyPassPhrase.DIALOG_PASS_PHRASE);
+ } else {
+ encryptStart();
+ }
+ }
+
+ public void passPhraseCallback(String passPhrase) {
+ Apg.setPassPhrase(passPhrase);
+ encryptStart();
+ }
+
+ private void encryptStart() {
+ showDialog(DIALOG_ENCRYPTING);
+ mRunningThread = new Thread(this);
+ mRunningThread.start();
+ }
+
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+ String message = mMessage.getText().toString();
+ // 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+", "");
+ message = message.replaceFirst("\n+$", "");
+ ByteArrayInputStream in =
+ new ByteArrayInputStream(Strings.toUTF8ByteArray(message));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ try {
+ if (mEncryptionKeyIds != null && mEncryptionKeyIds.length > 0) {
+ Apg.encrypt(in, out, mEncryptionKeyIds, mSignatureKeyId, Apg.getPassPhrase(), this);
+ data.putString("message", new String(out.toByteArray()));
+ } else {
+ Apg.sign(in, out, mSignatureKeyId, Apg.getPassPhrase(), this);
+ data.putString("message", message);
+ data.putString("signature", new String(out.toByteArray()));
+ }
+ } catch (IOException e) {
+ error = e.getMessage();
+ } catch (PGPException e) {
+ error = e.getMessage();
+ } catch (NoSuchProviderException e) {
+ error = e.getMessage();
+ } catch (NoSuchAlgorithmException e) {
+ error = e.getMessage();
+ } catch (SignatureException e) {
+ error = e.getMessage();
+ } catch (Apg.GeneralException e) {
+ error = e.getMessage();
+ }
+
+ data.putInt("type", MESSAGE_DONE);
+
+ if (error != null) {
+ data.putString("error", error);
+ }
+
+ msg.setData(data);
+ mhandler.sendMessage(msg);
+ }
+
+ private void updateView() {
+ if (mEncryptionKeyIds == null || mEncryptionKeyIds.length == 0) {
+ mSelectKeysButton.setText(R.string.no_keys_selected);
+ } else if (mEncryptionKeyIds.length == 1) {
+ mSelectKeysButton.setText(R.string.one_key_selected);
+ } else {
+ mSelectKeysButton.setText("" + mEncryptionKeyIds.length + " " +
+ getResources().getString(R.string.n_keys_selected));
+ }
+
+ if (mSignatureKeyId == 0) {
+ mSign.setText(R.string.sign);
+ mSign.setChecked(false);
+ mMainUserId.setText("");
+ mMainUserIdRest.setText("");
+ } else {
+ String uid = getResources().getString(R.string.unknown_user_id);
+ String uidExtra = "";
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRing(mSignatureKeyId);
+ if (keyRing != null) {
+ PGPSecretKey key = Apg.getMasterKey(keyRing);
+ if (key != null) {
+ String userId = Apg.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.setText(R.string.sign_as);
+ mSign.setChecked(true);
+ }
+ }
+
+ private void selectPublicKeys() {
+ Intent intent = new Intent(this, SelectPublicKeyListActivity.class);
+ intent.putExtra("selection", mEncryptionKeyIds);
+ startActivityForResult(intent, GET_PUCLIC_KEYS);
+ }
+
+ private void selectSecretKey() {
+ Intent intent = new Intent(this, SelectSecretKeyListActivity.class);
+ startActivityForResult(intent, GET_SECRET_KEY);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case GET_PUCLIC_KEYS: {
+ if (resultCode == RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ mEncryptionKeyIds = bundle.getLongArray("selection");
+ updateView();
+ }
+ break;
+ }
+
+ case GET_SECRET_KEY: {
+ if (resultCode == RESULT_OK) {
+ Bundle bundle = data.getExtras();
+ long newId = bundle.getLong("selectedKeyId");
+ if (mSignatureKeyId != newId) {
+ Apg.setPassPhrase(null);
+ }
+ mSignatureKeyId = newId;
+ } else {
+ mSignatureKeyId = 0;
+ Apg.setPassPhrase(null);
+ }
+ updateView();
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+} \ No newline at end of file
diff --git a/src/org/thialfihar/android/apg/MailListActivity.java b/src/org/thialfihar/android/apg/MailListActivity.java
new file mode 100644
index 000000000..8e63a7920
--- /dev/null
+++ b/src/org/thialfihar/android/apg/MailListActivity.java
@@ -0,0 +1,202 @@
+/*
+ * 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.thialfihar.android.apg;
+
+import java.util.Vector;
+import java.util.regex.Matcher;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+import android.widget.AdapterView.OnItemClickListener;
+
+public class MailListActivity extends ListActivity {
+ LayoutInflater minflater = null;
+
+ private class Conversation {
+ public long id;
+ public String subject;
+ public Vector<Message> messages;
+
+ public Conversation(long id, String subject) {
+ this.id = id;
+ this.subject = subject;
+ }
+ }
+
+ private class Message {
+ public Conversation parent;
+ public long id;
+ public String subject;
+ public String fromAddress;
+ public String data;
+ public String replyTo;
+
+ public Message(Conversation parent, long id, String subject,
+ String fromAddress, String replyTo, String data) {
+ this.parent = parent;
+ this.id = id;
+ this.subject = subject;
+ this.fromAddress = fromAddress;
+ this.replyTo = replyTo;
+ this.data = data;
+ if (this.replyTo == null || this.replyTo.equals("")) {
+ this.replyTo = this.fromAddress;
+ }
+ }
+ }
+
+ private Vector<Conversation> mconversations;
+ private Vector<Message> mmessages;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ minflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ mconversations = new Vector<Conversation>();
+ mmessages = new Vector<Message>();
+
+ String account = getIntent().getExtras().getString("account");
+ // TODO: what if account is null?
+ Uri uri = Uri.parse("content://gmail-ls/conversations/" + account);
+ Cursor cursor =
+ managedQuery(uri, new String[] { "conversation_id", "subject" }, null, null, null);
+ for (int i = 0; i < cursor.getCount(); ++i) {
+ cursor.moveToPosition(i);
+
+ int idIndex = cursor.getColumnIndex("conversation_id");
+ int subjectIndex = cursor.getColumnIndex("subject");
+ long conversationId = cursor.getLong(idIndex);
+ Conversation conversation =
+ new Conversation(conversationId, cursor.getString(subjectIndex));
+ Uri messageUri = Uri.withAppendedPath(uri, "" + conversationId + "/messages");
+ Cursor messageCursor =
+ managedQuery(messageUri, new String[] {
+ "messageId",
+ "subject",
+ "fromAddress",
+ "replyToAddresses",
+ "body" }, null, null, null);
+ Vector<Message> messages = new Vector<Message>();
+ for (int j = 0; j < messageCursor.getCount(); ++j) {
+ messageCursor.moveToPosition(j);
+ idIndex = messageCursor.getColumnIndex("messageId");
+ subjectIndex = messageCursor.getColumnIndex("subject");
+ int fromAddressIndex = messageCursor.getColumnIndex("fromAddress");
+ int replyToIndex = messageCursor.getColumnIndex("replyToAddresses");
+ int bodyIndex = messageCursor.getColumnIndex("body");
+ String data = messageCursor.getString(bodyIndex);
+ data = Html.fromHtml(data).toString();
+ Matcher matcher = Apg.PGP_MESSAGE.matcher(data);
+ if (matcher.matches()) {
+ data = matcher.group(1);
+ } else {
+ data = null;
+ }
+ Message message =
+ new Message(conversation,
+ messageCursor.getLong(idIndex),
+ messageCursor.getString(subjectIndex),
+ messageCursor.getString(fromAddressIndex),
+ messageCursor.getString(replyToIndex), data);
+
+ messages.add(message);
+ mmessages.add(message);
+ }
+ conversation.messages = messages;
+ mconversations.add(conversation);
+ }
+
+ setListAdapter(new MailboxAdapter());
+ getListView().setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View v, int position, long id) {
+ Intent intent = new Intent(MailListActivity.this, DecryptMessageActivity.class);
+ intent.setAction(Apg.Intent.DECRYPT);
+ Message message = (Message) ((MailboxAdapter) getListAdapter()).getItem(position);
+ intent.putExtra("data", message.data);
+ intent.putExtra("subject", message.subject);
+ intent.putExtra("replyTo", message.replyTo);
+ startActivity(intent);
+ }
+ });
+ }
+
+ private class MailboxAdapter extends BaseAdapter implements ListAdapter {
+
+ @Override
+ public boolean isEnabled(int position) {
+ Message message = (Message) getItem(position);
+ return message.data != null;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ return mmessages.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mmessages.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mmessages.get(position).id;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = minflater.inflate(R.layout.mailbox_message_item, null);
+
+ Message message = (Message) getItem(position);
+
+ TextView subject = (TextView) view.findViewById(R.id.subject);
+ TextView email = (TextView) view.findViewById(R.id.email_address);
+ ImageView encrypted = (ImageView) view.findViewById(R.id.ic_encrypted);
+
+ subject.setText(message.subject);
+ email.setText(message.fromAddress);
+ if (message.data != null) {
+ encrypted.setVisibility(View.VISIBLE);
+ } else {
+ encrypted.setVisibility(View.INVISIBLE);
+ }
+
+ return view;
+ }
+ }
+}
diff --git a/src/org/thialfihar/android/apg/MainActivity.java b/src/org/thialfihar/android/apg/MainActivity.java
new file mode 100644
index 000000000..e7107f255
--- /dev/null
+++ b/src/org/thialfihar/android/apg/MainActivity.java
@@ -0,0 +1,394 @@
+/*
+ * 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.thialfihar.android.apg;
+
+import org.thialfihar.android.apg.provider.Accounts;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.SpannableString;
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.CursorAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.OnItemClickListener;
+
+public class MainActivity extends Activity {
+ private static final int DIALOG_NEW_ACCOUNT = 1;
+ private static final int DIALOG_ABOUT = 2;
+ private static final int DIALOG_CHANGE_LOG = 3;
+
+ private static final int OPTION_MENU_ADD_ACCOUNT = 1;
+ private static final int OPTION_MENU_ABOUT = 2;
+ private static final int OPTION_MENU_MANAGE_PUBLIC_KEYS = 3;
+ private static final int OPTION_MENU_MANAGE_SECRET_KEYS = 4;
+
+ private static final int MENU_DELETE_ACCOUNT = 1;
+
+ private static String PREF_SEEN_CHANGE_LOG = "seenChangeLogDialog" + Apg.VERSION;
+
+ private ListView mAccounts = null;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+
+ Button encryptMessageButton = (Button) findViewById(R.id.btn_encryptMessage);
+ Button decryptMessageButton = (Button) findViewById(R.id.btn_decryptMessage);
+ mAccounts = (ListView) findViewById(R.id.account_list);
+
+ encryptMessageButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startEncryptMessageActivity();
+ }
+ });
+
+ decryptMessageButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startDecryptMessageActivity();
+ }
+ });
+
+ Cursor accountCursor = managedQuery(Accounts.CONTENT_URI, null, null, null, null);
+
+ mAccounts.setAdapter(new AccountListAdapter(this, accountCursor));
+ mAccounts.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View view, int index, long id) {
+ Cursor cursor =
+ managedQuery(Uri.withAppendedPath(Accounts.CONTENT_URI, "" + id), null,
+ null, null, null);
+ if (cursor != null && cursor.getCount() > 0) {
+ cursor.moveToFirst();
+ int nameIndex = cursor.getColumnIndex(Accounts.NAME);
+ String accountName = cursor.getString(nameIndex);
+ startMailListActivity(accountName);
+ }
+ }
+ });
+ registerForContextMenu(mAccounts);
+
+ SharedPreferences prefs = getPreferences(MODE_PRIVATE);
+ if (!prefs.getBoolean(PREF_SEEN_CHANGE_LOG, false)) {
+ showDialog(DIALOG_CHANGE_LOG);
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case DIALOG_NEW_ACCOUNT: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle("Add Account");
+ alert.setMessage("Specify the Google Mail account you want to add.");
+
+ final EditText input = new EditText(this);
+ alert.setView(input);
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(DIALOG_NEW_ACCOUNT);
+ String accountName = "" + input.getText();
+
+ Cursor testCursor =
+ managedQuery(Uri.parse("content://gmail-ls/conversations/" +
+ accountName),
+ null, null, null, null);
+ if (testCursor == null) {
+ Toast.makeText(MainActivity.this,
+ "Error: account '" + accountName +
+ "' not found",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(Accounts.NAME, accountName);
+ try {
+ MainActivity.this.getContentResolver()
+ .insert(Accounts.CONTENT_URI,
+ values);
+ } catch (SQLException e) {
+ Toast.makeText(MainActivity.this,
+ "Error: failed to add account '" +
+ accountName + "'",
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(DIALOG_NEW_ACCOUNT);
+ }
+ });
+
+ return alert.create();
+ }
+
+ case DIALOG_ABOUT: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle("About " + Apg.FULL_VERSION);
+ ScrollView scrollView = new ScrollView(this);
+ TextView message = new TextView(this);
+
+ SpannableString info =
+ new SpannableString("This is an attempt to bring OpenPGP to Android. " +
+ "It is far from complete, but more features are " +
+ "planned (see website).\n" +
+ "\n" +
+ "Feel free to send bug reports, suggestions, feature " +
+ "requests, feedback, photographs.\n" +
+ "\n" +
+ "mail: thi@thialfihar.org\n" +
+ "site: http://apg.thialfihar.org\n" +
+ "\n" +
+ "This software is provided \"as is\", without " +
+ "warranty of any kind.");
+ Linkify.addLinks(info, Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES);
+ message.setMovementMethod(LinkMovementMethod.getInstance());
+ message.setText(info);
+ // 5dip padding
+ int padding = (int) (10 * getResources().getDisplayMetrics().densityDpi / 160);
+ message.setPadding(padding, padding, padding, padding);
+ message.setTextAppearance(this, android.R.style.TextAppearance_Medium);
+ scrollView.addView(message);
+ alert.setView(scrollView);
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(DIALOG_ABOUT);
+ }
+ });
+
+ return alert.create();
+ }
+
+ case DIALOG_CHANGE_LOG: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle("Changes " + Apg.FULL_VERSION);
+ ScrollView scrollView = new ScrollView(this);
+ TextView message = new TextView(this);
+
+ SpannableString info =
+ new SpannableString("Read the warnings!\n\n" +
+ "Changes:\n" +
+ " * create/edit keys\n" +
+ " * export keys\n" +
+ " * GUI more Android-like\n" +
+ " * better error handling\n" +
+ " * bug fixes, optimizations\n" +
+ " * starting with v0.8.0 APG will be open source, see website\n" +
+ "\n" +
+ "WARNING: be careful editing your existing keys, as they " +
+ "WILL be stripped of certificates right now.\n" +
+ "WARNING: key creation/editing doesn't support all " +
+ "GPG features yet. In particular: " +
+ "key cross-certification is NOT supported, so signing " +
+ "with those keys will get a warning when the signature is " +
+ "checked.\n" +
+ "\n" +
+ "I hope APG continues to be useful to you, please send " +
+ "bug reports, feature wishes, feedback.");
+ message.setText(info);
+ // 5dip padding
+ int padding = (int) (10 * getResources().getDisplayMetrics().densityDpi / 160);
+ message.setPadding(padding, padding, padding, padding);
+ message.setTextAppearance(this, android.R.style.TextAppearance_Medium);
+ scrollView.addView(message);
+ alert.setView(scrollView);
+
+ alert.setCancelable(false);
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ MainActivity.this.removeDialog(DIALOG_CHANGE_LOG);
+ SharedPreferences prefs = getPreferences(MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREF_SEEN_CHANGE_LOG, true);
+ editor.commit();
+ }
+ });
+
+ return alert.create();
+ }
+
+ default: {
+ break;
+ }
+ }
+ return super.onCreateDialog(id);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, OPTION_MENU_MANAGE_PUBLIC_KEYS, 0, R.string.menu_managePublicKeys)
+ .setIcon(android.R.drawable.ic_menu_manage);
+ menu.add(0, OPTION_MENU_MANAGE_SECRET_KEYS, 1, R.string.menu_manageSecretKeys)
+ .setIcon(android.R.drawable.ic_menu_manage);
+ menu.add(1, OPTION_MENU_ADD_ACCOUNT, 2, R.string.menu_addAccount)
+ .setIcon(android.R.drawable.ic_menu_add);
+ menu.add(1, OPTION_MENU_ABOUT, 3, R.string.menu_about)
+ .setIcon(android.R.drawable.ic_menu_info_details);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case OPTION_MENU_ADD_ACCOUNT: {
+ showDialog(DIALOG_NEW_ACCOUNT);
+ return true;
+ }
+
+ case OPTION_MENU_ABOUT: {
+ showDialog(DIALOG_ABOUT);
+ return true;
+ }
+
+ case OPTION_MENU_MANAGE_PUBLIC_KEYS: {
+ startPublicKeyManager();
+ return true;
+ }
+
+ case OPTION_MENU_MANAGE_SECRET_KEYS: {
+ startSecretKeyManager();
+ return true;
+ }
+
+ default: {
+ break;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+
+ TextView nameTextView = (TextView) v.findViewById(R.id.account_name);
+ if (nameTextView != null) {
+ menu.setHeaderTitle(nameTextView.getText());
+ menu.add(0, MENU_DELETE_ACCOUNT, 0, "Delete Account");
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ AdapterView.AdapterContextMenuInfo info =
+ (AdapterView.AdapterContextMenuInfo) menuItem.getMenuInfo();
+
+ switch (menuItem.getItemId()) {
+ case MENU_DELETE_ACCOUNT: {
+ Uri uri = Uri.withAppendedPath(Accounts.CONTENT_URI, "" + info.id);
+ this.getContentResolver().delete(uri, null, null);
+ return true;
+ }
+
+ default: {
+ return super.onContextItemSelected(menuItem);
+ }
+ }
+ }
+
+ public void startPublicKeyManager() {
+ startActivity(new Intent(this, PublicKeyListActivity.class));
+ }
+
+ public void startSecretKeyManager() {
+ startActivity(new Intent(this, SecretKeyListActivity.class));
+ //startActivity(new Intent(this, EditKeyActivity.class));
+ }
+
+ public void startEncryptMessageActivity() {
+ startActivity(new Intent(this, EncryptMessageActivity.class));
+ }
+
+ public void startDecryptMessageActivity() {
+ startActivity(new Intent(this, DecryptMessageActivity.class));
+ }
+
+ public void startMailListActivity(String account) {
+ startActivity(new Intent(this, MailListActivity.class).putExtra("account", account));
+ }
+
+ private class AccountListAdapter extends CursorAdapter {
+ private LayoutInflater minflater;
+
+ public AccountListAdapter(Context context, Cursor cursor) {
+ super(context, cursor);
+ minflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public int getCount() {
+ return super.getCount();
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return minflater.inflate(R.layout.account_item, null);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ TextView nameTextView = (TextView) view.findViewById(R.id.account_name);
+ int nameIndex = cursor.getColumnIndex(Accounts.NAME);
+ final String account = cursor.getString(nameIndex);
+ nameTextView.setText(account);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/org/thialfihar/android/apg/ProgressDialogUpdater.java b/src/org/thialfihar/android/apg/ProgressDialogUpdater.java
new file mode 100644
index 000000000..bdc8055ed
--- /dev/null
+++ b/src/org/thialfihar/android/apg/ProgressDialogUpdater.java
@@ -0,0 +1,22 @@
+/*
+ * 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.thialfihar.android.apg;
+
+public interface ProgressDialogUpdater {
+ void setProgress(String message, int current, int total);
+ void setProgress(int current, int total);
+}
diff --git a/src/org/thialfihar/android/apg/PublicKeyListActivity.java b/src/org/thialfihar/android/apg/PublicKeyListActivity.java
new file mode 100644
index 000000000..cebd9c7ae
--- /dev/null
+++ b/src/org/thialfihar/android/apg/PublicKeyListActivity.java
@@ -0,0 +1,660 @@
+/*
+ * 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.thialfihar.android.apg;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Vector;
+
+import org.bouncycastle2.openpgp.PGPException;
+import org.bouncycastle2.openpgp.PGPPublicKey;
+import org.bouncycastle2.openpgp.PGPPublicKeyRing;
+import org.thialfihar.android.apg.utils.IterableIterator;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ExpandableListActivity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnKeyListener;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.EditText;
+import android.widget.ExpandableListView;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+public class PublicKeyListActivity extends ExpandableListActivity
+ implements Runnable, ProgressDialogUpdater {
+ static final int MENU_DELETE = 1;
+ static final int MENU_EXPORT = 2;
+
+ static final int OPTION_MENU_IMPORT_KEYS = 1;
+ static final int OPTION_MENU_EXPORT_KEYS = 2;
+
+ static final int MESSAGE_PROGRESS_UPDATE = 1;
+ static final int MESSAGE_IMPORT_DONE = 2;
+ static final int MESSAGE_EXPORT_DONE = 3;
+
+ static final int DIALOG_DELETE_KEY = 1;
+ static final int DIALOG_IMPORT_KEYS = 2;
+ static final int DIALOG_IMPORTING = 3;
+ static final int DIALOG_EXPORT_KEYS = 4;
+ static final int DIALOG_EXPORTING = 5;
+ static final int DIALOG_EXPORT_KEY = 6;
+
+ static final int TASK_IMPORT = 1;
+ static final int TASK_EXPORT = 2;
+
+ protected int mSelectedItem = -1;
+ protected String mImportFilename = null;
+ protected String mExportFilename = null;
+ protected int mTask = 0;
+
+ private ProgressDialog mProgressDialog = null;
+ private Thread mRunningThread = null;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ Bundle data = msg.getData();
+ if (data != null) {
+ int type = data.getInt("type");
+ switch (type) {
+ case MESSAGE_PROGRESS_UPDATE: {
+ String message = data.getString("message");
+ if (mProgressDialog != null) {
+ if (message != null) {
+ mProgressDialog.setMessage(message);
+ }
+ mProgressDialog.setMax(data.getInt("max"));
+ mProgressDialog.setProgress(data.getInt("progress"));
+ }
+ break;
+ }
+
+ case MESSAGE_IMPORT_DONE: {
+ removeDialog(DIALOG_IMPORTING);
+ mProgressDialog = null;
+
+ String error = data.getString("error");
+ if (error != null) {
+ Toast.makeText(PublicKeyListActivity.this,
+ "Error: " + data.getString("error"),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ int added = data.getInt("added");
+ int updated = data.getInt("updated");
+ String message;
+ if (added > 0 && updated > 0) {
+ message = "Succssfully added " + added + " keys and updated " +
+ updated + " keys.";
+ } else if (added > 0) {
+ message = "Succssfully added " + added + " keys.";
+ } else if (updated > 0) {
+ message = "Succssfully updated " + updated + " keys.";
+ } else {
+ message = "No keys added or updated.";
+ }
+ Toast.makeText(PublicKeyListActivity.this, message,
+ Toast.LENGTH_SHORT).show();
+ }
+ refreshList();
+ break;
+ }
+
+ case MESSAGE_EXPORT_DONE: {
+ removeDialog(DIALOG_EXPORTING);
+ mProgressDialog = null;
+
+ String error = data.getString("error");
+ if (error != null) {
+ Toast.makeText(PublicKeyListActivity.this,
+ "Error: " + data.getString("error"),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ int exported = data.getInt("exported");
+ String message;
+ if (exported == 1) {
+ message = "Succssfully exported 1 key.";
+ } else if (exported > 0) {
+ message = "Succssfully exported " + exported + " keys.";
+ } else{
+ message = "No keys exported.";
+ }
+ Toast.makeText(PublicKeyListActivity.this, message,
+ Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ public void setProgress(int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt("type", MESSAGE_PROGRESS_UPDATE);
+ data.putInt("progress", progress);
+ data.putInt("max", max);
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ public void setProgress(String message, int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt("type", MESSAGE_PROGRESS_UPDATE);
+ data.putString("message", message);
+ data.putInt("progress", progress);
+ data.putInt("max", max);
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Apg.initialize(this);
+
+ setListAdapter(new PublicKeyListAdapter(this));
+ registerForContextMenu(getExpandableListView());
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, OPTION_MENU_IMPORT_KEYS, 0, "Import Keys")
+ .setIcon(android.R.drawable.ic_menu_add);
+ menu.add(0, OPTION_MENU_EXPORT_KEYS, 1, "Export Keys")
+ .setIcon(android.R.drawable.ic_menu_save);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case OPTION_MENU_IMPORT_KEYS: {
+ showDialog(DIALOG_IMPORT_KEYS);
+ return true;
+ }
+
+ case OPTION_MENU_EXPORT_KEYS: {
+ showDialog(DIALOG_EXPORT_KEYS);
+ return true;
+ }
+
+ default: {
+ break;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ ExpandableListView.ExpandableListContextMenuInfo info =
+ (ExpandableListView.ExpandableListContextMenuInfo) menuInfo;
+ int type = ExpandableListView.getPackedPositionType(info.packedPosition);
+ int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
+
+ if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+ PGPPublicKeyRing keyRing = Apg.getPublicKeyRings().get(groupPosition);
+ String userId = Apg.getMainUserIdSafe(this, Apg.getMasterKey(keyRing));
+ menu.setHeaderTitle(userId);
+ menu.add(0, MENU_EXPORT, 0, "Export Key");
+ menu.add(0, MENU_DELETE, 1, "Delete Key");
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuItem.getMenuInfo();
+ int type = ExpandableListView.getPackedPositionType(info.packedPosition);
+ int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
+
+ if (type != ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+ return super.onContextItemSelected(menuItem);
+ }
+
+ switch (menuItem.getItemId()) {
+ case MENU_EXPORT: {
+ mSelectedItem = groupPosition;
+ showDialog(DIALOG_EXPORT_KEY);
+ return true;
+ }
+
+ case MENU_DELETE: {
+ mSelectedItem = groupPosition;
+ showDialog(DIALOG_DELETE_KEY);
+ return true;
+ }
+
+ default: {
+ return super.onContextItemSelected(menuItem);
+ }
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ boolean singleKeyExport = false;
+
+ switch (id) {
+ case DIALOG_DELETE_KEY: {
+ PGPPublicKeyRing keyRing = Apg.getPublicKeyRings().get(mSelectedItem);
+ String userId = Apg.getMainUserIdSafe(this, Apg.getMasterKey(keyRing));
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Warning ");
+ builder.setMessage("Do you really want to delete the key '" + userId + "'?\n" +
+ "You can't undo this!");
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setPositiveButton("Delete", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ deleteKey(mSelectedItem);
+ mSelectedItem = -1;
+ removeDialog(DIALOG_DELETE_KEY);
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ mSelectedItem = -1;
+ removeDialog(DIALOG_DELETE_KEY);
+ }
+ });
+ return builder.create();
+ }
+
+ case DIALOG_IMPORT_KEYS: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle("Import Keys");
+ alert.setMessage("Please specify which file to import from.");
+
+ final EditText input = new EditText(this);
+ // TODO: default file?
+ input.setText(Environment.getExternalStorageDirectory() + "/pubring.gpg");
+ input.setOnKeyListener(new OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // TODO: this doesn't actually work yet
+ // If the event is a key-down event on the "enter"
+ // button
+ if ((event.getAction() == KeyEvent.ACTION_DOWN) &&
+ (keyCode == KeyEvent.KEYCODE_ENTER)) {
+ try {
+ ((AlertDialog) v.getParent())
+ .getButton(AlertDialog.BUTTON_POSITIVE)
+ .performClick();
+ } catch (ClassCastException e) {
+ // don't do anything if we're not in that dialog
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+ alert.setView(input);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(DIALOG_IMPORT_KEYS);
+ mImportFilename = input.getText().toString();
+ importKeys();
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(DIALOG_IMPORT_KEYS);
+ }
+ });
+ return alert.create();
+ }
+
+ case DIALOG_EXPORT_KEY: {
+ singleKeyExport = true;
+ // break intentionally omitted, to use the DIALOG_EXPORT_KEYS dialog
+ }
+
+ case DIALOG_EXPORT_KEYS: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ if (singleKeyExport) {
+ alert.setTitle("Export Key");
+ } else {
+ alert.setTitle("Export Keys");
+ mSelectedItem = -1;
+ }
+ final int thisDialogId = (singleKeyExport ? DIALOG_DELETE_KEY : DIALOG_EXPORT_KEYS);
+ alert.setMessage("Please specify which file to export to.\n" +
+ "WARNING! File will be overwritten if it exists.");
+
+ final EditText input = new EditText(this);
+ // TODO: default file?
+ input.setText(Environment.getExternalStorageDirectory() + "/pubexport.asc");
+ alert.setView(input);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(thisDialogId);
+ mExportFilename = input.getText().toString();
+ exportKeys();
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(thisDialogId);
+ }
+ });
+ return alert.create();
+ }
+
+ case DIALOG_IMPORTING: {
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setMessage("importing...");
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+ }
+
+ case DIALOG_EXPORTING: {
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setMessage("exporting...");
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+ }
+ }
+ return super.onCreateDialog(id);
+ }
+
+ public void importKeys() {
+ showDialog(DIALOG_IMPORTING);
+ mTask = TASK_IMPORT;
+ mRunningThread = new Thread(this);
+ mRunningThread.start();
+ }
+
+ public void exportKeys() {
+ showDialog(DIALOG_EXPORTING);
+ mTask = TASK_EXPORT;
+ mRunningThread = new Thread(this);
+ mRunningThread.start();
+ }
+
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ String filename = null;
+ if (mTask == TASK_IMPORT) {
+ filename = mImportFilename;
+ } else {
+ filename = mExportFilename;
+ }
+
+ try {
+ if (mTask == TASK_IMPORT) {
+ data = Apg.importKeyRings(this, Apg.TYPE_PUBLIC, filename, this);
+ } else {
+ Vector<Object> keys = new Vector<Object>();
+ if (mSelectedItem == -1) {
+ for (PGPPublicKeyRing key : Apg.getPublicKeyRings()) {
+ keys.add(key);
+ }
+ } else {
+ keys.add(Apg.getPublicKeyRings().get(mSelectedItem));
+ }
+ data = Apg.exportKeyRings(this, keys, filename, this);
+ }
+ } catch (FileNotFoundException e) {
+ error = "file '" + filename + "' not found";
+ } catch (IOException e) {
+ error = e.getMessage();
+ } catch (PGPException e) {
+ error = e.getMessage();
+ } catch (Apg.GeneralException e) {
+ error = e.getMessage();
+ }
+
+ if (mTask == TASK_IMPORT) {
+ data.putInt("type", MESSAGE_IMPORT_DONE);
+ } else {
+ data.putInt("type", MESSAGE_EXPORT_DONE);
+ }
+
+ if (error != null) {
+ data.putString("error", error);
+ }
+
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ private void deleteKey(int index) {
+ PGPPublicKeyRing keyRing = Apg.getPublicKeyRings().get(index);
+ Apg.deleteKey(this, keyRing);
+ refreshList();
+ }
+
+ private void refreshList() {
+ ((PublicKeyListAdapter) getExpandableListAdapter()).notifyDataSetChanged();
+ }
+
+ private class PublicKeyListAdapter extends BaseExpandableListAdapter {
+ private LayoutInflater mInflater;
+
+ private class KeyChild {
+ public static final int KEY = 0;
+ public static final int USER_ID = 1;
+
+ public int type;
+ public PGPPublicKey key;
+ public String userId;
+
+ public KeyChild(PGPPublicKey key) {
+ type = KEY;
+ this.key = key;
+ }
+
+ public KeyChild(String userId) {
+ type = USER_ID;
+ this.userId = userId;
+ }
+ }
+
+ public PublicKeyListAdapter(Context context) {
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ protected Vector<KeyChild> getChildrenOfKeyRing(PGPPublicKeyRing keyRing) {
+ Vector<KeyChild> children = new Vector<KeyChild>();
+ PGPPublicKey masterKey = null;
+ for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
+ children.add(new KeyChild(key));
+ if (key.isMasterKey()) {
+ masterKey = key;
+ }
+ }
+
+ if (masterKey != null) {
+ boolean isFirst = true;
+ for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
+ if (isFirst) {
+ // ignore first, it's in the group already
+ isFirst = false;
+ continue;
+ }
+ children.add(new KeyChild(userId));
+ }
+ }
+
+ return children;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ public int getGroupCount() {
+ return Apg.getPublicKeyRings().size();
+ }
+
+ public Object getChild(int groupPosition, int childPosition) {
+ PGPPublicKeyRing keyRing = Apg.getPublicKeyRings().get(groupPosition);
+ Vector<KeyChild> children = getChildrenOfKeyRing(keyRing);
+
+ KeyChild child = children.get(childPosition);
+ return child;
+ }
+
+ public long getChildId(int groupPosition, int childPosition) {
+ return childPosition;
+ }
+
+ public int getChildrenCount(int groupPosition) {
+ return getChildrenOfKeyRing(Apg.getPublicKeyRings().get(groupPosition)).size();
+ }
+
+ public Object getGroup(int position) {
+ return position;
+ }
+
+ public long getGroupId(int position) {
+ return position;
+ }
+
+ public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+ ViewGroup parent) {
+ PGPPublicKeyRing keyRing = Apg.getPublicKeyRings().get(groupPosition);
+ for (PGPPublicKey key : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
+ View view;
+ if (!key.isMasterKey()) {
+ continue;
+ }
+ view = mInflater.inflate(R.layout.key_list_group_item, null);
+ view.setBackgroundResource(android.R.drawable.list_selector_background);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.main_user_id);
+ mainUserId.setText("");
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.main_user_id_rest);
+ mainUserIdRest.setText("");
+
+ String userId = Apg.getMainUserId(key);
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ if (mainUserId.getText().length() == 0) {
+ mainUserId.setText(R.string.unknown_user_id);
+ }
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+ return view;
+ }
+ return null;
+ }
+
+ public View getChildView(int groupPosition, int childPosition,
+ boolean isLastChild, View convertView,
+ ViewGroup parent) {
+ PGPPublicKeyRing keyRing = Apg.getPublicKeyRings().get(groupPosition);
+ Vector<KeyChild> children = getChildrenOfKeyRing(keyRing);
+
+ KeyChild child = children.get(childPosition);
+ View view = null;
+ switch (child.type) {
+ case KeyChild.KEY: {
+ PGPPublicKey key = child.key;
+ if (key.isMasterKey()) {
+ view = mInflater.inflate(R.layout.key_list_child_item_master_key, null);
+ } else {
+ view = mInflater.inflate(R.layout.key_list_child_item_sub_key, null);
+ }
+
+ TextView keyId = (TextView) view.findViewById(R.id.key_id);
+ String keyIdStr = Long.toHexString(key.getKeyID() & 0xffffffffL);
+ while (keyIdStr.length() < 8) {
+ keyIdStr = "0" + keyIdStr;
+ }
+ keyId.setText(keyIdStr);
+ TextView keyDetails = (TextView) view.findViewById(R.id.key_details);
+ String algorithmStr = Apg.getAlgorithmInfo(key);
+ keyDetails.setText("(" + algorithmStr + ")");
+
+ ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encrypt_key);
+ if (!Apg.isEncryptionKey(key)) {
+ encryptIcon.setVisibility(View.GONE);
+ }
+
+ ImageView signIcon = (ImageView) view.findViewById(R.id.ic_sign_key);
+ if (!Apg.isSigningKey(key)) {
+ signIcon.setVisibility(View.GONE);
+ }
+ break;
+ }
+
+ case KeyChild.USER_ID: {
+ view = mInflater.inflate(R.layout.key_list_child_item_user_id, null);
+ TextView userId = (TextView) view.findViewById(R.id.user_id);
+ userId.setText(child.userId);
+ break;
+ }
+ }
+ return view;
+ }
+ }
+}
diff --git a/src/org/thialfihar/android/apg/SecretKeyListActivity.java b/src/org/thialfihar/android/apg/SecretKeyListActivity.java
new file mode 100644
index 000000000..f42b4ccad
--- /dev/null
+++ b/src/org/thialfihar/android/apg/SecretKeyListActivity.java
@@ -0,0 +1,758 @@
+/*
+ * 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.thialfihar.android.apg;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Vector;
+
+import org.bouncycastle2.openpgp.PGPException;
+import org.bouncycastle2.openpgp.PGPSecretKey;
+import org.bouncycastle2.openpgp.PGPSecretKeyRing;
+import org.thialfihar.android.apg.utils.IterableIterator;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ExpandableListActivity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View.OnKeyListener;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.EditText;
+import android.widget.ExpandableListView;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.ExpandableListView.OnChildClickListener;
+
+public class SecretKeyListActivity extends ExpandableListActivity
+ implements Runnable, ProgressDialogUpdater, OnChildClickListener,
+ AskForSecretKeyPassPhrase.PassPhraseCallbackInterface {
+ static final int CREATE_SECRET_KEY = 1;
+ static final int EDIT_SECRET_KEY = 2;
+
+ static final int MENU_EDIT = 1;
+ static final int MENU_EXPORT = 2;
+ static final int MENU_DELETE = 3;
+
+ static final int OPTION_MENU_IMPORT_KEYS = 1;
+ static final int OPTION_MENU_EXPORT_KEYS = 2;
+ static final int OPTION_MENU_CREATE_KEY = 3;
+
+ static final int MESSAGE_PROGRESS_UPDATE = 1;
+ static final int MESSAGE_DONE = 2;
+ static final int MESSAGE_IMPORT_DONE = 2;
+ static final int MESSAGE_EXPORT_DONE = 3;
+
+ static final int DIALOG_DELETE_KEY = 1;
+ static final int DIALOG_IMPORT_KEYS = 2;
+ static final int DIALOG_IMPORTING = 3;
+ static final int DIALOG_EXPORT_KEYS = 4;
+ static final int DIALOG_EXPORTING = 5;
+ static final int DIALOG_EXPORT_KEY = 6;
+
+ static final int TASK_IMPORT = 1;
+ static final int TASK_EXPORT = 2;
+
+ protected int mSelectedItem = -1;
+ protected String mImportFilename = null;
+ protected String mExportFilename = null;
+ protected int mTask = 0;
+
+ private ProgressDialog mProgressDialog = null;
+ private Thread mRunningThread = null;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ Bundle data = msg.getData();
+ if (data != null) {
+ int type = data.getInt("type");
+ switch (type) {
+ case MESSAGE_PROGRESS_UPDATE: {
+ String message = data.getString("message");
+ if (mProgressDialog != null) {
+ if (message != null) {
+ mProgressDialog.setMessage(message);
+ }
+ mProgressDialog.setMax(data.getInt("max"));
+ mProgressDialog.setProgress(data.getInt("progress"));
+ }
+ break;
+ }
+
+ case MESSAGE_IMPORT_DONE: {
+ removeDialog(DIALOG_IMPORTING);
+ mProgressDialog = null;
+
+ String error = data.getString("error");
+ if (error != null) {
+ Toast.makeText(SecretKeyListActivity.this,
+ "Error: " + data.getString("error"),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ int added = data.getInt("added");
+ int updated = data.getInt("updated");
+ String message;
+ if (added > 0 && updated > 0) {
+ message = "Succssfully added " + added + " keys and updated " +
+ updated + " keys.";
+ } else if (added > 0) {
+ message = "Succssfully added " + added + " keys.";
+ } else if (updated > 0) {
+ message = "Succssfully updated " + updated + " keys.";
+ } else {
+ message = "No keys added or updated.";
+ }
+ Toast.makeText(SecretKeyListActivity.this, message,
+ Toast.LENGTH_SHORT).show();
+ }
+ refreshList();
+ break;
+ }
+
+ case MESSAGE_EXPORT_DONE: {
+ removeDialog(DIALOG_EXPORTING);
+ mProgressDialog = null;
+
+ String error = data.getString("error");
+ if (error != null) {
+ Toast.makeText(SecretKeyListActivity.this,
+ "Error: " + data.getString("error"),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ int exported = data.getInt("exported");
+ String message;
+ if (exported == 1) {
+ message = "Succssfully exported 1 key.";
+ } else if (exported > 0) {
+ message = "Succssfully exported " + exported + " keys.";
+ } else{
+ message = "No keys exported.";
+ }
+ Toast.makeText(SecretKeyListActivity.this, message,
+ Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+
+ default: {
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ public void setProgress(int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt("type", MESSAGE_PROGRESS_UPDATE);
+ data.putInt("progress", progress);
+ data.putInt("max", max);
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ public void setProgress(String message, int progress, int max) {
+ Message msg = new Message();
+ Bundle data = new Bundle();
+ data.putInt("type", MESSAGE_PROGRESS_UPDATE);
+ data.putString("message", message);
+ data.putInt("progress", progress);
+ data.putInt("max", max);
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Apg.initialize(this);
+
+ setListAdapter(new SecretKeyListAdapter(this));
+ registerForContextMenu(getExpandableListView());
+ getExpandableListView().setOnChildClickListener(this);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, OPTION_MENU_IMPORT_KEYS, 0, "Import Keys")
+ .setIcon(android.R.drawable.ic_menu_add);
+ menu.add(0, OPTION_MENU_EXPORT_KEYS, 1, "Export Keys")
+ .setIcon(android.R.drawable.ic_menu_save);
+ menu.add(1, OPTION_MENU_CREATE_KEY, 2, "Create Key")
+ .setIcon(android.R.drawable.ic_menu_add);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case OPTION_MENU_IMPORT_KEYS: {
+ showDialog(DIALOG_IMPORT_KEYS);
+ return true;
+ }
+
+ case OPTION_MENU_EXPORT_KEYS: {
+ showDialog(DIALOG_EXPORT_KEYS);
+ return true;
+ }
+
+ case OPTION_MENU_CREATE_KEY: {
+ createKey();
+ return true;
+ }
+
+ default: {
+ break;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ ExpandableListView.ExpandableListContextMenuInfo info =
+ (ExpandableListView.ExpandableListContextMenuInfo) menuInfo;
+ int type = ExpandableListView.getPackedPositionType(info.packedPosition);
+ int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
+
+ if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRings().get(groupPosition);
+ String userId = Apg.getMainUserIdSafe(this, Apg.getMasterKey(keyRing));
+ menu.setHeaderTitle(userId);
+ menu.add(0, MENU_EDIT, 0, "Edit Key");
+ menu.add(0, MENU_EXPORT, 1, "Export Key");
+ menu.add(0, MENU_DELETE, 2, "Delete Key");
+ }
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem menuItem) {
+ ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuItem.getMenuInfo();
+ int type = ExpandableListView.getPackedPositionType(info.packedPosition);
+ int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
+
+ if (type != ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
+ return super.onContextItemSelected(menuItem);
+ }
+
+ switch (menuItem.getItemId()) {
+ case MENU_EDIT: {
+ mSelectedItem = groupPosition;
+ showDialog(AskForSecretKeyPassPhrase.DIALOG_PASS_PHRASE);
+ return true;
+ }
+
+ case MENU_EXPORT: {
+ mSelectedItem = groupPosition;
+ showDialog(DIALOG_EXPORT_KEY);
+ return true;
+ }
+
+ case MENU_DELETE: {
+ mSelectedItem = groupPosition;
+ showDialog(DIALOG_DELETE_KEY);
+ return true;
+ }
+
+ default: {
+ return super.onContextItemSelected(menuItem);
+ }
+ }
+ }
+
+ @Override
+ public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+ int childPosition, long id) {
+ mSelectedItem = groupPosition;
+ showDialog(AskForSecretKeyPassPhrase.DIALOG_PASS_PHRASE);
+ return true;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ boolean singleKeyExport = false;
+
+ switch (id) {
+ case DIALOG_DELETE_KEY: {
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRings().get(mSelectedItem);
+
+ String userId = Apg.getMainUserIdSafe(this, Apg.getMasterKey(keyRing));
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Warning ");
+ builder.setMessage("Do you really want to delete the key '" + userId + "'?\n" +
+ "You can't undo this!");
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setPositiveButton("Delete", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ deleteKey(mSelectedItem);
+ mSelectedItem = -1;
+ removeDialog(DIALOG_DELETE_KEY);
+ }
+ });
+ builder.setNegativeButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ mSelectedItem = -1;
+ removeDialog(DIALOG_DELETE_KEY);
+ }
+ });
+ return builder.create();
+ }
+
+ case DIALOG_IMPORT_KEYS: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ alert.setTitle("Import Keys");
+ alert.setMessage("Please specify which file to import from.");
+
+ final EditText input = new EditText(this);
+ // TODO: default file?
+ input.setText(Environment.getExternalStorageDirectory() + "/secring.gpg");
+ input.setOnKeyListener(new OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // TODO: this doesn't actually work yet
+ // If the event is a key-down event on the "enter"
+ // button
+ if ((event.getAction() == KeyEvent.ACTION_DOWN) &&
+ (keyCode == KeyEvent.KEYCODE_ENTER)) {
+ try {
+ ((AlertDialog) v.getParent())
+ .getButton(AlertDialog.BUTTON_POSITIVE)
+ .performClick();
+ } catch (ClassCastException e) {
+ // don't do anything if we're not in that dialog
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+ alert.setView(input);
+
+ alert.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(DIALOG_IMPORT_KEYS);
+ mImportFilename = input.getText().toString();
+ importKeys();
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(DIALOG_IMPORT_KEYS);
+ }
+ });
+ return alert.create();
+ }
+
+ case DIALOG_EXPORT_KEY: {
+ singleKeyExport = true;
+ // break intentionally omitted, to use the DIALOG_EXPORT_KEYS dialog
+ }
+
+ case DIALOG_EXPORT_KEYS: {
+ AlertDialog.Builder alert = new AlertDialog.Builder(this);
+
+ if (singleKeyExport) {
+ alert.setTitle("Export Key");
+ } else {
+ alert.setTitle("Export Keys");
+ mSelectedItem = -1;
+ }
+ final int thisDialogId = (singleKeyExport ? DIALOG_DELETE_KEY : DIALOG_EXPORT_KEYS);
+ alert.setMessage("Please specify which file to export to.\n" +
+ "WARNING! You are about to export a SECRET key.\n" +
+ "WARNING! File will be overwritten if it exists.");
+
+ final EditText input = new EditText(this);
+ // TODO: default file?
+ input.setText(Environment.getExternalStorageDirectory() + "/secexport.asc");
+ input.setOnKeyListener(new OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // TODO: this doesn't actually work yet
+ // If the event is a key-down event on the "enter"
+ // button
+ if ((event.getAction() == KeyEvent.ACTION_DOWN) &&
+ (keyCode == KeyEvent.KEYCODE_ENTER)) {
+ try {
+ ((AlertDialog) v.getParent())
+ .getButton(AlertDialog.BUTTON_POSITIVE)
+ .performClick();
+ } catch (ClassCastException e) {
+ // don't do anything if we're not in that dialog
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+ alert.setView(input);
+
+ alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(thisDialogId);
+ mExportFilename = input.getText().toString();
+ exportKeys();
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ removeDialog(thisDialogId);
+ }
+ });
+ return alert.create();
+ }
+
+ case DIALOG_IMPORTING: {
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setMessage("importing...");
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+ }
+
+ case DIALOG_EXPORTING: {
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setMessage("exporting...");
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+ }
+
+ case AskForSecretKeyPassPhrase.DIALOG_PASS_PHRASE: {
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRings().get(mSelectedItem);
+ long keyId = keyRing.getSecretKey().getKeyID();
+ return AskForSecretKeyPassPhrase.createDialog(this, keyId, this);
+ }
+ }
+ return super.onCreateDialog(id);
+ }
+
+ public void passPhraseCallback(String passPhrase) {
+ Apg.setPassPhrase(passPhrase);
+ editKey();
+ }
+
+ private void createKey() {
+ Intent intent = new Intent(this, EditKeyActivity.class);
+ startActivityForResult(intent, CREATE_SECRET_KEY);
+ }
+
+ private void editKey() {
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRings().get(mSelectedItem);
+ long keyId = keyRing.getSecretKey().getKeyID();
+ Intent intent = new Intent(this, EditKeyActivity.class);
+ intent.putExtra("keyId", keyId);
+ startActivityForResult(intent, EDIT_SECRET_KEY);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case CREATE_SECRET_KEY: // intentionally no break
+ case EDIT_SECRET_KEY: {
+ if (resultCode == RESULT_OK) {
+ refreshList();
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ public void importKeys() {
+ showDialog(DIALOG_IMPORTING);
+ mTask = TASK_IMPORT;
+ mRunningThread = new Thread(this);
+ mRunningThread.start();
+ }
+
+ public void exportKeys() {
+ showDialog(DIALOG_EXPORTING);
+ mTask = TASK_EXPORT;
+ mRunningThread = new Thread(this);
+ mRunningThread.start();
+ }
+
+ public void run() {
+ String error = null;
+ Bundle data = new Bundle();
+ Message msg = new Message();
+
+ String filename = null;
+ if (mTask == TASK_IMPORT) {
+ filename = mImportFilename;
+ } else {
+ filename = mExportFilename;
+ }
+
+ try {
+ if (mTask == TASK_IMPORT) {
+ data = Apg.importKeyRings(this, Apg.TYPE_SECRET, filename, this);
+ } else {
+ Vector<Object> keys = new Vector<Object>();
+ if (mSelectedItem == -1) {
+ for (PGPSecretKeyRing key : Apg.getSecretKeyRings()) {
+ keys.add(key);
+ }
+ } else {
+ keys.add(Apg.getSecretKeyRings().get(mSelectedItem));
+ }
+ data = Apg.exportKeyRings(this, keys, filename, this);
+ }
+ } catch (FileNotFoundException e) {
+ error = "file '" + filename + "' not found";
+ } catch (IOException e) {
+ error = e.getMessage();
+ } catch (PGPException e) {
+ error = e.getMessage();
+ } catch (Apg.GeneralException e) {
+ error = e.getMessage();
+ }
+
+ if (mTask == TASK_IMPORT) {
+ data.putInt("type", MESSAGE_IMPORT_DONE);
+ } else {
+ data.putInt("type", MESSAGE_EXPORT_DONE);
+ }
+
+ if (error != null) {
+ data.putString("error", error);
+ }
+
+ msg.setData(data);
+ mHandler.sendMessage(msg);
+ }
+
+ private void deleteKey(int index) {
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRings().get(index);
+ Apg.deleteKey(this, keyRing);
+ refreshList();
+ }
+
+ private void refreshList() {
+ ((SecretKeyListAdapter) getExpandableListAdapter())
+ .notifyDataSetChanged();
+ }
+
+ private class SecretKeyListAdapter extends BaseExpandableListAdapter {
+ private LayoutInflater mInflater;
+
+ private class KeyChild {
+ static final int KEY = 0;
+ static final int USER_ID = 1;
+
+ public int type;
+ public PGPSecretKey key;
+ public String userId;
+
+ public KeyChild(PGPSecretKey key) {
+ type = KEY;
+ this.key = key;
+ }
+
+ public KeyChild(String userId) {
+ type = USER_ID;
+ this.userId = userId;
+ }
+ }
+
+ public SecretKeyListAdapter(Context context) {
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ protected Vector<KeyChild> getChildrenOfKeyRing(PGPSecretKeyRing keyRing) {
+ Vector<KeyChild> children = new Vector<KeyChild>();
+ PGPSecretKey masterKey = null;
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ children.add(new KeyChild(key));
+ if (key.isMasterKey()) {
+ masterKey = key;
+ }
+ }
+
+ if (masterKey != null) {
+ boolean isFirst = true;
+ for (String userId : new IterableIterator<String>(masterKey.getUserIDs())) {
+ if (isFirst) {
+ // ignore first, it's in the group already
+ isFirst = false;
+ continue;
+ }
+ children.add(new KeyChild(userId));
+ }
+ }
+
+ return children;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ public int getGroupCount() {
+ return Apg.getSecretKeyRings().size();
+ }
+
+ public Object getChild(int groupPosition, int childPosition) {
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRings().get(groupPosition);
+ Vector<KeyChild> children = getChildrenOfKeyRing(keyRing);
+ KeyChild child = children.get(childPosition);
+ return child;
+ }
+
+ public long getChildId(int groupPosition, int childPosition) {
+ return childPosition;
+ }
+
+ public int getChildrenCount(int groupPosition) {
+ return getChildrenOfKeyRing(Apg.getSecretKeyRings().get(groupPosition)).size();
+ }
+
+ public Object getGroup(int position) {
+ return position;
+ }
+
+ public long getGroupId(int position) {
+ return position;
+ }
+
+ public View getGroupView(int groupPosition, boolean isExpanded,
+ View convertView, ViewGroup parent) {
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRings().get(groupPosition);
+ for (PGPSecretKey key : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ View view;
+ if (!key.isMasterKey()) {
+ continue;
+ }
+ view = mInflater.inflate(R.layout.key_list_group_item, null);
+ view.setBackgroundResource(android.R.drawable.list_selector_background);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.main_user_id);
+ mainUserId.setText("");
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.main_user_id_rest);
+ mainUserIdRest.setText("");
+
+ String userId = Apg.getMainUserId(key);
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ if (mainUserId.getText().length() == 0) {
+ mainUserId.setText(R.string.unknown_user_id);
+ }
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+ return view;
+ }
+ return null;
+ }
+
+ public View getChildView(int groupPosition, int childPosition,
+ boolean isLastChild, View convertView,
+ ViewGroup parent) {
+ PGPSecretKeyRing keyRing = Apg.getSecretKeyRings().get(groupPosition);
+ Vector<KeyChild> children = getChildrenOfKeyRing(keyRing);
+
+ KeyChild child = children.get(childPosition);
+ View view = null;
+ switch (child.type) {
+ case KeyChild.KEY: {
+ PGPSecretKey key = child.key;
+ if (key.isMasterKey()) {
+ view = mInflater.inflate(R.layout.key_list_child_item_master_key, null);
+ } else {
+ view = mInflater.inflate(R.layout.key_list_child_item_sub_key, null);
+ }
+
+ TextView keyId = (TextView) view.findViewById(R.id.key_id);
+ String keyIdStr = Long.toHexString(key.getKeyID() & 0xffffffffL);
+ while (keyIdStr.length() < 8) {
+ keyIdStr = "0" + keyIdStr;
+ }
+ keyId.setText(keyIdStr);
+ TextView keyDetails = (TextView) view.findViewById(R.id.key_details);
+ String algorithmStr = Apg.getAlgorithmInfo(key);
+ keyDetails.setText("(" + algorithmStr + ")");
+
+ ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encrypt_key);
+ if (!Apg.isEncryptionKey(key)) {
+ encryptIcon.setVisibility(View.GONE);
+ }
+
+ ImageView signIcon = (ImageView) view.findViewById(R.id.ic_sign_key);
+ if (!Apg.isSigningKey(key)) {
+ signIcon.setVisibility(View.GONE);
+ }
+ break;
+ }
+
+ case KeyChild.USER_ID: {
+ view = mInflater.inflate(R.layout.key_list_child_item_user_id, null);
+ TextView userId = (TextView) view.findViewById(R.id.user_id);
+ userId.setText(child.userId);
+ break;
+ }
+ }
+ return view;
+ }
+ }
+}
diff --git a/src/org/thialfihar/android/apg/SelectPublicKeyListActivity.java b/src/org/thialfihar/android/apg/SelectPublicKeyListActivity.java
new file mode 100644
index 000000000..551d9508e
--- /dev/null
+++ b/src/org/thialfihar/android/apg/SelectPublicKeyListActivity.java
@@ -0,0 +1,259 @@
+/*
+ * 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.thialfihar.android.apg;
+
+import java.text.DateFormat;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Vector;
+
+import org.bouncycastle2.openpgp.PGPPublicKey;
+import org.bouncycastle2.openpgp.PGPPublicKeyRing;
+import org.thialfihar.android.apg.utils.IterableIterator;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class SelectPublicKeyListActivity extends Activity {
+ protected Vector<PGPPublicKeyRing> mKeyRings;
+ protected LayoutInflater mInflater;
+ protected Intent mIntent;
+ protected ListView mList;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ // fill things
+ mIntent = getIntent();
+ long selectedKeyIds[] = null;
+ if (mIntent.getExtras() != null) {
+ selectedKeyIds = mIntent.getExtras().getLongArray("selection");
+ }
+
+ Apg.initialize(this);
+ mKeyRings = (Vector<PGPPublicKeyRing>) Apg.getPublicKeyRings().clone();
+ Collections.sort(mKeyRings, new Apg.PublicKeySorter());
+
+ setContentView(R.layout.select_public_key);
+
+ mList = (ListView) findViewById(R.id.list);
+ mList.setAdapter(new PublicKeyListAdapter(this));
+ if (selectedKeyIds != null) {
+ for (int i = 0; i < mKeyRings.size(); ++i) {
+ PGPPublicKeyRing keyRing = mKeyRings.get(i);
+ PGPPublicKey key = Apg.getMasterKey(keyRing);
+ if (key == null) {
+ continue;
+ }
+ for (int j = 0; j < selectedKeyIds.length; ++j) {
+ if (key.getKeyID() == selectedKeyIds[j]) {
+ mList.setItemChecked(i, true);
+ break;
+ }
+ }
+ }
+ }
+
+ Button okButton = (Button) findViewById(R.id.btn_ok);
+
+ okButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ okClicked();
+ }
+ });
+
+ Button cancelButton = (Button) findViewById(R.id.btn_cancel);
+
+ cancelButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cancelClicked();
+ }
+ });
+ }
+
+ private void cancelClicked() {
+ setResult(RESULT_CANCELED, null);
+ finish();
+ }
+
+ private void okClicked() {
+ Intent data = new Intent();
+ Vector<Long> vector = new Vector<Long>();
+ for (int i = 0; i < mList.getCount(); ++i) {
+ if (mList.isItemChecked(i)) {
+ vector.add(mList.getItemIdAtPosition(i));
+ }
+ }
+ long selectedKeyIds[] = new long[vector.size()];
+ for (int i = 0; i < vector.size(); ++i) {
+ selectedKeyIds[i] = vector.get(i);
+ }
+ data.putExtra("selection", selectedKeyIds);
+ setResult(RESULT_OK, data);
+ finish();
+ }
+
+ private class PublicKeyListAdapter extends BaseAdapter {
+ public PublicKeyListAdapter(Context context) {
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ PGPPublicKeyRing keyRing = mKeyRings.get(position);
+
+ if (Apg.getMasterKey(keyRing) == null) {
+ return false;
+ }
+
+ Vector<PGPPublicKey> encryptKeys = Apg.getUsableEncryptKeys(keyRing);
+ if (encryptKeys.size() == 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ return mKeyRings.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mKeyRings.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ PGPPublicKeyRing keyRing = mKeyRings.get(position);
+ PGPPublicKey key = Apg.getMasterKey(keyRing);
+ if (key != null) {
+ return key.getKeyID();
+ }
+
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = mInflater.inflate(R.layout.select_public_key_item, null);
+ boolean enabled = isEnabled(position);
+
+ PGPPublicKeyRing keyRing = mKeyRings.get(position);
+ PGPPublicKey key = null;
+ for (PGPPublicKey tKey : new IterableIterator<PGPPublicKey>(keyRing.getPublicKeys())) {
+ if (tKey.isMasterKey()) {
+ key = tKey;
+ break;
+ }
+ }
+
+ Vector<PGPPublicKey> encryptKeys = Apg.getEncryptKeys(keyRing);
+ Vector<PGPPublicKey> usableKeys = Apg.getUsableEncryptKeys(keyRing);
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.main_user_id);
+ mainUserId.setText(R.string.unknown_user_id);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.main_user_id_rest);
+ mainUserIdRest.setText("");
+ TextView keyId = (TextView) view.findViewById(R.id.key_id);
+ keyId.setText("<no key>");
+ TextView creation = (TextView) view.findViewById(R.id.creation);
+ creation.setText("-");
+ TextView expiry = (TextView) view.findViewById(R.id.expiry);
+ expiry.setText("no expire");
+ TextView status = (TextView) view.findViewById(R.id.status);
+ status.setText("???");
+
+ if (key != null) {
+ String userId = Apg.getMainUserId(key);
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ keyId.setText("" + Long.toHexString(key.getKeyID() & 0xffffffffL));
+ }
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ PGPPublicKey timespanKey = key;
+ if (usableKeys.size() > 0) {
+ timespanKey = usableKeys.get(0);
+ status.setText("can encrypt");
+ } else if (encryptKeys.size() > 0) {
+ timespanKey = encryptKeys.get(0);
+ Date now = new Date();
+ if (now.compareTo(Apg.getCreationDate(timespanKey)) > 0) {
+ status.setText("not valid");
+ } else {
+ status.setText("expired");
+ }
+ } else {
+ status.setText("no key");
+ }
+
+ creation.setText(DateFormat.getDateInstance().format(Apg.getCreationDate(timespanKey)));
+ Date expiryDate = Apg.getExpiryDate(timespanKey);
+ if (expiryDate != null) {
+ expiry.setText(DateFormat.getDateInstance().format(expiryDate));
+ }
+
+ status.setText(status.getText() + " ");
+
+ CheckBox selected = (CheckBox) view.findViewById(R.id.selected);
+ selected.setChecked(mList.isItemChecked(position));
+
+ view.setEnabled(enabled);
+ mainUserId.setEnabled(enabled);
+ mainUserIdRest.setEnabled(enabled);
+ keyId.setEnabled(enabled);
+ creation.setEnabled(enabled);
+ expiry.setEnabled(enabled);
+ selected.setEnabled(enabled);
+ status.setEnabled(enabled);
+
+ return view;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/org/thialfihar/android/apg/SelectSecretKeyListActivity.java b/src/org/thialfihar/android/apg/SelectSecretKeyListActivity.java
new file mode 100644
index 000000000..da7094c53
--- /dev/null
+++ b/src/org/thialfihar/android/apg/SelectSecretKeyListActivity.java
@@ -0,0 +1,209 @@
+/*
+ * 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.thialfihar.android.apg;
+
+import java.text.DateFormat;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Vector;
+
+import org.bouncycastle2.openpgp.PGPSecretKey;
+import org.bouncycastle2.openpgp.PGPSecretKeyRing;
+import org.thialfihar.android.apg.utils.IterableIterator;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.AdapterView.OnItemClickListener;
+
+public class SelectSecretKeyListActivity extends Activity {
+ protected Vector<PGPSecretKeyRing> mKeyRings;
+ protected LayoutInflater mInflater;
+ protected Intent mIntent;
+ protected ListView mList;
+
+ protected long mSelectedKeyId = 0;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ // fill things
+ mIntent = getIntent();
+
+ Apg.initialize(this);
+
+ mKeyRings = (Vector<PGPSecretKeyRing>) Apg.getSecretKeyRings().clone();
+ Collections.sort(mKeyRings, new Apg.SecretKeySorter());
+
+ setContentView(R.layout.select_secret_key);
+
+ mList = (ListView) findViewById(R.id.list);
+ mList.setAdapter(new SecretKeyListAdapter(this));
+
+ mList.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
+ Intent data = new Intent();
+ data.putExtra("selectedKeyId", id);
+ setResult(RESULT_OK, data);
+ finish();
+ }
+ });
+ }
+
+ private class SecretKeyListAdapter extends BaseAdapter {
+
+ public SecretKeyListAdapter(Context context) {
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ PGPSecretKeyRing keyRing = mKeyRings.get(position);
+
+ if (Apg.getMasterKey(keyRing) == null) {
+ return false;
+ }
+
+ Vector<PGPSecretKey> usableKeys = Apg.getUsableSigningKeys(keyRing);
+ if (usableKeys.size() == 0) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ return mKeyRings.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mKeyRings.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ PGPSecretKeyRing keyRing = mKeyRings.get(position);
+ PGPSecretKey key = Apg.getMasterKey(keyRing);
+ if (key != null) {
+ return key.getKeyID();
+ }
+
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = mInflater.inflate(R.layout.select_secret_key_item, null);
+ boolean enabled = isEnabled(position);
+
+ PGPSecretKeyRing keyRing = mKeyRings.get(position);
+ PGPSecretKey key = null;
+ for (PGPSecretKey tKey : new IterableIterator<PGPSecretKey>(keyRing.getSecretKeys())) {
+ if (tKey.isMasterKey()) {
+ key = tKey;
+ break;
+ }
+ }
+
+ TextView mainUserId = (TextView) view.findViewById(R.id.main_user_id);
+ mainUserId.setText(R.string.unknown_user_id);
+ TextView mainUserIdRest = (TextView) view.findViewById(R.id.main_user_id_rest);
+ mainUserIdRest.setText("");
+ TextView keyId = (TextView) view.findViewById(R.id.key_id);
+ keyId.setText("<no key>");
+ TextView creation = (TextView) view.findViewById(R.id.creation);
+ creation.setText("");
+ TextView expiry = (TextView) view.findViewById(R.id.expiry);
+ expiry.setText("");
+ TextView status = (TextView) view.findViewById(R.id.status);
+ status.setText("???");
+
+ if (key != null) {
+ String userId = Apg.getMainUserId(key);
+ if (userId != null) {
+ String chunks[] = userId.split(" <", 2);
+ userId = chunks[0];
+ if (chunks.length > 1) {
+ mainUserIdRest.setText("<" + chunks[1]);
+ }
+ mainUserId.setText(userId);
+ }
+
+ keyId.setText("" + Long.toHexString(key.getKeyID() & 0xffffffffL));
+ }
+
+ if (mainUserIdRest.getText().length() == 0) {
+ mainUserIdRest.setVisibility(View.GONE);
+ }
+
+ Vector<PGPSecretKey> signingKeys = Apg.getSigningKeys(keyRing);
+ Vector<PGPSecretKey> usableKeys = Apg.getUsableSigningKeys(keyRing);
+
+ PGPSecretKey timespanKey = key;
+ if (usableKeys.size() > 0) {
+ timespanKey = usableKeys.get(0);
+ status.setText("can sign");
+ } else if (signingKeys.size() > 0) {
+ timespanKey = signingKeys.get(0);
+ Date now = new Date();
+ if (now.compareTo(Apg.getCreationDate(timespanKey)) > 0) {
+ status.setText("not valid");
+ } else {
+ status.setText("expired");
+ }
+ } else {
+ status.setText("no key");
+ }
+
+ creation.setText(DateFormat.getDateInstance().format(Apg.getCreationDate(timespanKey)));
+ Date expiryDate = Apg.getExpiryDate(timespanKey);
+ if (expiryDate != null) {
+ expiry.setText(DateFormat.getDateInstance().format(expiryDate));
+ }
+
+ status.setText(status.getText() + " ");
+
+ view.setEnabled(enabled);
+ mainUserId.setEnabled(enabled);
+ mainUserIdRest.setEnabled(enabled);
+ keyId.setEnabled(enabled);
+ creation.setEnabled(enabled);
+ expiry.setEnabled(enabled);
+ status.setEnabled(enabled);
+
+ return view;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/org/thialfihar/android/apg/provider/Accounts.java b/src/org/thialfihar/android/apg/provider/Accounts.java
new file mode 100644
index 000000000..4fce2b607
--- /dev/null
+++ b/src/org/thialfihar/android/apg/provider/Accounts.java
@@ -0,0 +1,22 @@
+/*
+ * 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.thialfihar.android.apg.provider;
+
+public class Accounts extends Accounts1 {
+ private Accounts() {
+ }
+} \ No newline at end of file
diff --git a/src/org/thialfihar/android/apg/provider/Accounts1.java b/src/org/thialfihar/android/apg/provider/Accounts1.java
new file mode 100644
index 000000000..9009e4598
--- /dev/null
+++ b/src/org/thialfihar/android/apg/provider/Accounts1.java
@@ -0,0 +1,36 @@
+/*
+ * 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.thialfihar.android.apg.provider;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+class Accounts1 implements BaseColumns {
+ public static final String TABLE_NAME = "accounts";
+
+ public static final String _ID_type = "INTEGER PRIMARY KEY";
+ public static final String NAME = "c_name";
+ public static final String NAME_type = "TEXT";
+
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + DataProvider.AUTHORITY + "/accounts");
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/vnd.thialfihar.apg.account";
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/vnd.thialfihar.apg.account";
+ public static final String DEFAULT_SORT_ORDER = _ID + " DESC";
+} \ No newline at end of file
diff --git a/src/org/thialfihar/android/apg/provider/DataProvider.java b/src/org/thialfihar/android/apg/provider/DataProvider.java
new file mode 100644
index 000000000..0a6a814e4
--- /dev/null
+++ b/src/org/thialfihar/android/apg/provider/DataProvider.java
@@ -0,0 +1,494 @@
+/*
+ * 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.thialfihar.android.apg.provider;
+
+import java.util.HashMap;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.text.TextUtils;
+
+public class DataProvider extends ContentProvider {
+ public static final String AUTHORITY = "org.thialfihar.android.apg.provider";
+
+ private static final String DATABASE_NAME = "apg";
+ private static final int DATABASE_VERSION = 1;
+
+ private static final int PUBLIC_KEYS = 101;
+ private static final int PUBLIC_KEY_ID = 102;
+ private static final int PUBLIC_KEY_BY_KEY_ID = 103;
+
+ private static final int SECRET_KEYS = 201;
+ private static final int SECRET_KEY_ID = 202;
+ private static final int SECRET_KEY_BY_KEY_ID = 203;
+
+ private static final int ACCOUNTS = 301;
+ private static final int ACCOUNT_ID = 302;
+
+ private static final UriMatcher mUriMatcher;
+ private static final HashMap<String, String> mPublicKeysProjectionMap;
+ private static final HashMap<String, String> mSecretKeysProjectionMap;
+ private static final HashMap<String, String> mAccountsProjectionMap;
+
+ private DatabaseHelper mdbHelper;
+
+ static {
+ mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ mUriMatcher.addURI(DataProvider.AUTHORITY, "public_keys", PUBLIC_KEYS);
+ mUriMatcher.addURI(DataProvider.AUTHORITY, "public_keys/#", PUBLIC_KEY_ID);
+ mUriMatcher.addURI(DataProvider.AUTHORITY, "public_keys/key_id/*", PUBLIC_KEY_BY_KEY_ID);
+
+ mUriMatcher.addURI(DataProvider.AUTHORITY, "secret_keys", SECRET_KEYS);
+ mUriMatcher.addURI(DataProvider.AUTHORITY, "secret_keys/#", SECRET_KEY_ID);
+ mUriMatcher.addURI(DataProvider.AUTHORITY, "secret_keys/key_id/*", SECRET_KEY_BY_KEY_ID);
+
+ mUriMatcher.addURI(DataProvider.AUTHORITY, "accounts", ACCOUNTS);
+ mUriMatcher.addURI(DataProvider.AUTHORITY, "accounts/#", ACCOUNT_ID);
+
+ mPublicKeysProjectionMap = new HashMap<String, String>();
+ mPublicKeysProjectionMap.put(PublicKeys._ID, PublicKeys._ID);
+ mPublicKeysProjectionMap.put(PublicKeys.KEY_ID, PublicKeys.KEY_ID);
+ mPublicKeysProjectionMap.put(PublicKeys.KEY_DATA, PublicKeys.KEY_DATA);
+ mPublicKeysProjectionMap.put(PublicKeys.WHO_ID, PublicKeys.WHO_ID);
+
+ mSecretKeysProjectionMap = new HashMap<String, String>();
+ mSecretKeysProjectionMap.put(PublicKeys._ID, PublicKeys._ID);
+ mSecretKeysProjectionMap.put(PublicKeys.KEY_ID, PublicKeys.KEY_ID);
+ mSecretKeysProjectionMap.put(PublicKeys.KEY_DATA, PublicKeys.KEY_DATA);
+ mSecretKeysProjectionMap.put(PublicKeys.WHO_ID, PublicKeys.WHO_ID);
+
+ mAccountsProjectionMap = new HashMap<String, String>();
+ mAccountsProjectionMap.put(Accounts._ID, Accounts._ID);
+ mAccountsProjectionMap.put(Accounts.NAME, Accounts.NAME);
+ }
+
+ /**
+ * This class helps open, create, and upgrade the database file.
+ */
+ private static class DatabaseHelper extends SQLiteOpenHelper {
+
+ DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + PublicKeys.TABLE_NAME + " (" +
+ PublicKeys._ID + " " + PublicKeys._ID_type + "," +
+ PublicKeys.KEY_ID + " " + PublicKeys.KEY_ID_type + ", " +
+ PublicKeys.KEY_DATA + " " + PublicKeys.KEY_DATA_type + ", " +
+ PublicKeys.WHO_ID + " " + PublicKeys.WHO_ID_type + ");");
+
+ db.execSQL("CREATE TABLE " + SecretKeys.TABLE_NAME + " (" +
+ SecretKeys._ID + " " + SecretKeys._ID_type + "," +
+ SecretKeys.KEY_ID + " " + SecretKeys.KEY_ID_type + ", " +
+ SecretKeys.KEY_DATA + " " + SecretKeys.KEY_DATA_type + ", " +
+ SecretKeys.WHO_ID + " " + SecretKeys.WHO_ID_type + ");");
+
+ db.execSQL("CREATE TABLE " + Accounts.TABLE_NAME + " (" +
+ Accounts._ID + " " + Accounts._ID_type + "," +
+ Accounts.NAME + " " + Accounts.NAME_type + ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ int currentVersion = oldVersion;
+ while (currentVersion < newVersion) {
+ switch (currentVersion) {
+ default: {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ mdbHelper = new DatabaseHelper(getContext());
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+
+ switch (mUriMatcher.match(uri)) {
+ case PUBLIC_KEYS: {
+ qb.setTables(PublicKeys.TABLE_NAME);
+ qb.setProjectionMap(mPublicKeysProjectionMap);
+ break;
+ }
+
+ case PUBLIC_KEY_ID: {
+ qb.setTables(PublicKeys.TABLE_NAME);
+ qb.setProjectionMap(mPublicKeysProjectionMap);
+ qb.appendWhere(PublicKeys._ID + "=" + uri.getPathSegments().get(1));
+ break;
+ }
+
+ case PUBLIC_KEY_BY_KEY_ID: {
+ qb.setTables(PublicKeys.TABLE_NAME);
+ qb.setProjectionMap(mPublicKeysProjectionMap);
+ qb.appendWhere(PublicKeys.KEY_ID + "=" + uri.getPathSegments().get(2));
+ break;
+ }
+
+ case SECRET_KEYS: {
+ qb.setTables(SecretKeys.TABLE_NAME);
+ qb.setProjectionMap(mSecretKeysProjectionMap);
+ break;
+ }
+
+ case SECRET_KEY_ID: {
+ qb.setTables(SecretKeys.TABLE_NAME);
+ qb.setProjectionMap(mSecretKeysProjectionMap);
+ qb.appendWhere(SecretKeys._ID + "=" + uri.getPathSegments().get(1));
+ break;
+ }
+
+ case SECRET_KEY_BY_KEY_ID: {
+ qb.setTables(SecretKeys.TABLE_NAME);
+ qb.setProjectionMap(mSecretKeysProjectionMap);
+ qb.appendWhere(SecretKeys.KEY_ID + "=" + uri.getPathSegments().get(2));
+ break;
+ }
+
+ case ACCOUNTS: {
+ qb.setTables(Accounts.TABLE_NAME);
+ qb.setProjectionMap(mAccountsProjectionMap);
+ break;
+ }
+
+ case ACCOUNT_ID: {
+ qb.setTables(Accounts.TABLE_NAME);
+ qb.setProjectionMap(mAccountsProjectionMap);
+ qb.appendWhere(Accounts._ID + "=" + uri.getPathSegments().get(1));
+ break;
+ }
+
+ default: {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ }
+
+ // If no sort order is specified use the default
+ String orderBy;
+ if (TextUtils.isEmpty(sortOrder)) {
+ orderBy = PublicKeys.DEFAULT_SORT_ORDER;
+ } else {
+ orderBy = sortOrder;
+ }
+
+ // Get the database and run the query
+ SQLiteDatabase db = mdbHelper.getReadableDatabase();
+ 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);
+ return c;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ switch (mUriMatcher.match(uri)) {
+ case PUBLIC_KEYS: {
+ return PublicKeys.CONTENT_TYPE;
+ }
+
+ case PUBLIC_KEY_ID: {
+ return PublicKeys.CONTENT_ITEM_TYPE;
+ }
+
+ case PUBLIC_KEY_BY_KEY_ID: {
+ return PublicKeys.CONTENT_ITEM_TYPE;
+ }
+
+ case SECRET_KEYS: {
+ return SecretKeys.CONTENT_TYPE;
+ }
+
+ case SECRET_KEY_ID: {
+ return SecretKeys.CONTENT_ITEM_TYPE;
+ }
+
+ case SECRET_KEY_BY_KEY_ID: {
+ return SecretKeys.CONTENT_ITEM_TYPE;
+ }
+
+ case ACCOUNTS: {
+ return Accounts.CONTENT_TYPE;
+ }
+
+ case ACCOUNT_ID: {
+ return Accounts.CONTENT_ITEM_TYPE;
+ }
+
+ default: {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ }
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues initialValues) {
+ switch (mUriMatcher.match(uri)) {
+ case PUBLIC_KEYS: {
+ ContentValues values;
+ if (initialValues != null) {
+ values = new ContentValues(initialValues);
+ } else {
+ values = new ContentValues();
+ }
+
+ if (!values.containsKey(PublicKeys.WHO_ID)) {
+ values.put(PublicKeys.WHO_ID, "");
+ }
+
+ SQLiteDatabase db = mdbHelper.getWritableDatabase();
+ long rowId = db.insert(PublicKeys.TABLE_NAME, PublicKeys.WHO_ID, values);
+ if (rowId > 0) {
+ Uri transferUri = ContentUris.withAppendedId(PublicKeys.CONTENT_URI, rowId);
+ getContext().getContentResolver().notifyChange(transferUri, null);
+ return transferUri;
+ }
+
+ throw new SQLException("Failed to insert row into " + uri);
+ }
+
+ case SECRET_KEYS: {
+ ContentValues values;
+ if (initialValues != null) {
+ values = new ContentValues(initialValues);
+ } else {
+ values = new ContentValues();
+ }
+
+ if (!values.containsKey(SecretKeys.WHO_ID)) {
+ values.put(SecretKeys.WHO_ID, "");
+ }
+
+ SQLiteDatabase db = mdbHelper.getWritableDatabase();
+ long rowId = db.insert(SecretKeys.TABLE_NAME, SecretKeys.WHO_ID, values);
+ if (rowId > 0) {
+ Uri transferUri = ContentUris.withAppendedId(SecretKeys.CONTENT_URI, rowId);
+ getContext().getContentResolver().notifyChange(transferUri, null);
+ return transferUri;
+ }
+
+ throw new SQLException("Failed to insert row into " + uri);
+ }
+
+ case ACCOUNTS: {
+ ContentValues values;
+ if (initialValues != null) {
+ values = new ContentValues(initialValues);
+ } else {
+ values = new ContentValues();
+ }
+
+ SQLiteDatabase db = mdbHelper.getWritableDatabase();
+ long rowId = db.insert(Accounts.TABLE_NAME, null, values);
+ if (rowId > 0) {
+ Uri transferUri = ContentUris.withAppendedId(Accounts.CONTENT_URI, rowId);
+ getContext().getContentResolver().notifyChange(transferUri, null);
+ return transferUri;
+ }
+
+ throw new SQLException("Failed to insert row into " + uri);
+ }
+
+ default: {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ }
+ }
+
+ @Override
+ public int delete(Uri uri, String where, String[] whereArgs) {
+ SQLiteDatabase db = mdbHelper.getWritableDatabase();
+ int count;
+ switch (mUriMatcher.match(uri)) {
+ case PUBLIC_KEYS: {
+ count = db.delete(PublicKeys.TABLE_NAME, where, whereArgs);
+ break;
+ }
+
+ case PUBLIC_KEY_ID: {
+ String publicKeyId = uri.getPathSegments().get(1);
+ count = db.delete(PublicKeys.TABLE_NAME,
+ PublicKeys._ID + "=" + publicKeyId +
+ (!TextUtils.isEmpty(where) ?
+ " AND (" + where + ')' : ""),
+ whereArgs);
+ break;
+ }
+
+ case PUBLIC_KEY_BY_KEY_ID: {
+ String publicKeyKeyId = uri.getPathSegments().get(2);
+ count = db.delete(PublicKeys.TABLE_NAME,
+ PublicKeys.KEY_ID + "=" + publicKeyKeyId +
+ (!TextUtils.isEmpty(where) ?
+ " AND (" + where + ')' : ""),
+ whereArgs);
+ break;
+ }
+
+ case SECRET_KEYS: {
+ count = db.delete(SecretKeys.TABLE_NAME, where, whereArgs);
+ break;
+ }
+
+ case SECRET_KEY_ID: {
+ String secretKeyId = uri.getPathSegments().get(1);
+ count = db.delete(SecretKeys.TABLE_NAME,
+ SecretKeys._ID + "=" + secretKeyId +
+ (!TextUtils.isEmpty(where) ?
+ " AND (" + where + ')' : ""),
+ whereArgs);
+ break;
+ }
+
+ case SECRET_KEY_BY_KEY_ID: {
+ String secretKeyKeyId = uri.getPathSegments().get(2);
+ count = db.delete(SecretKeys.TABLE_NAME,
+ SecretKeys.KEY_ID + "=" + secretKeyKeyId +
+ (!TextUtils.isEmpty(where) ?
+ " AND (" + where + ')' : ""),
+ whereArgs);
+ break;
+ }
+
+ case ACCOUNTS: {
+ count = db.delete(Accounts.TABLE_NAME, where, whereArgs);
+ break;
+ }
+
+ case ACCOUNT_ID: {
+ String accountId = uri.getPathSegments().get(1);
+ count = db.delete(Accounts.TABLE_NAME,
+ Accounts._ID + "=" + accountId +
+ (!TextUtils.isEmpty(where) ?
+ " AND (" + where + ')' : ""),
+ whereArgs);
+ break;
+ }
+
+ default: {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ }
+
+ getContext().getContentResolver().notifyChange(uri, null);
+ return count;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
+ SQLiteDatabase db = mdbHelper.getWritableDatabase();
+ int count;
+ switch (mUriMatcher.match(uri)) {
+ case PUBLIC_KEYS: {
+ count = db.update(PublicKeys.TABLE_NAME, values, where, whereArgs);
+ break;
+ }
+
+ case PUBLIC_KEY_ID: {
+ String publicKeyId = uri.getPathSegments().get(1);
+
+ count = db.update(PublicKeys.TABLE_NAME, values,
+ PublicKeys._ID + "=" + publicKeyId +
+ (!TextUtils.isEmpty(where) ?
+ " AND (" + where + ')' : ""),
+ whereArgs);
+ break;
+ }
+
+ case PUBLIC_KEY_BY_KEY_ID: {
+ String publicKeyKeyId = uri.getPathSegments().get(2);
+
+ count = db.update(PublicKeys.TABLE_NAME, values,
+ PublicKeys.KEY_ID + "=" + publicKeyKeyId +
+ (!TextUtils.isEmpty(where) ?
+ " AND (" + where + ')' : ""),
+ whereArgs);
+ break;
+ }
+
+ case SECRET_KEYS: {
+ count = db.update(SecretKeys.TABLE_NAME, values, where, whereArgs);
+ break;
+ }
+
+ case SECRET_KEY_ID: {
+ String secretKeyId = uri.getPathSegments().get(1);
+
+ count = db.update(SecretKeys.TABLE_NAME, values,
+ SecretKeys._ID + "=" + secretKeyId +
+ (!TextUtils.isEmpty(where) ?
+ " AND (" + where + ')' : ""),
+ whereArgs);
+ break;
+ }
+
+ case SECRET_KEY_BY_KEY_ID: {
+ String secretKeyKeyId = uri.getPathSegments().get(2);
+
+ count = db.update(SecretKeys.TABLE_NAME, values,
+ SecretKeys.KEY_ID + "=" + secretKeyKeyId +
+ (!TextUtils.isEmpty(where) ?
+ " AND (" + where + ')' : ""),
+ whereArgs);
+ break;
+ }
+
+ case ACCOUNTS: {
+ count = db.update(Accounts.TABLE_NAME, values, where, whereArgs);
+ break;
+ }
+
+ case ACCOUNT_ID: {
+ String accountId = uri.getPathSegments().get(1);
+
+ count = db.update(Accounts.TABLE_NAME, values,
+ Accounts._ID + "=" + accountId +
+ (!TextUtils.isEmpty(where) ?
+ " AND (" + where + ')' : ""),
+ whereArgs);
+ break;
+ }
+
+ default: {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ }
+
+ getContext().getContentResolver().notifyChange(uri, null);
+ return count;
+ }
+}
diff --git a/src/org/thialfihar/android/apg/provider/PublicKeys.java b/src/org/thialfihar/android/apg/provider/PublicKeys.java
new file mode 100644
index 000000000..f15841fa5
--- /dev/null
+++ b/src/org/thialfihar/android/apg/provider/PublicKeys.java
@@ -0,0 +1,22 @@
+/*
+ * 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.thialfihar.android.apg.provider;
+
+public class PublicKeys extends PublicKeys1 {
+ private PublicKeys() {
+ }
+} \ No newline at end of file
diff --git a/src/org/thialfihar/android/apg/provider/PublicKeys1.java b/src/org/thialfihar/android/apg/provider/PublicKeys1.java
new file mode 100644
index 000000000..d12a67a17
--- /dev/null
+++ b/src/org/thialfihar/android/apg/provider/PublicKeys1.java
@@ -0,0 +1,42 @@
+/*
+ * 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.thialfihar.android.apg.provider;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+class PublicKeys1 implements BaseColumns {
+ public static final String TABLE_NAME = "public_keys";
+
+ public static final String _ID_type = "INTEGER PRIMARY KEY";
+ public static final String KEY_ID = "c_key_id";
+ public static final String KEY_ID_type = "INT64";
+ public static final String KEY_DATA = "c_key_data";
+ public static final String KEY_DATA_type = "BLOB";
+ public static final String WHO_ID = "c_who_id";
+ public static final String WHO_ID_type = "INTEGER";
+
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + DataProvider.AUTHORITY + "/public_keys");
+ public static final Uri CONTENT_URI_BY_KEY_ID =
+ Uri.parse("content://" + DataProvider.AUTHORITY + "/public_keys/key_id");
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/vnd.thialfihar.apg.public_key";
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/vnd.thialfihar.apg.public_key";
+ public static final String DEFAULT_SORT_ORDER = _ID + " DESC";
+} \ No newline at end of file
diff --git a/src/org/thialfihar/android/apg/provider/SecretKeys.java b/src/org/thialfihar/android/apg/provider/SecretKeys.java
new file mode 100644
index 000000000..d31f306ae
--- /dev/null
+++ b/src/org/thialfihar/android/apg/provider/SecretKeys.java
@@ -0,0 +1,22 @@
+/*
+ * 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.thialfihar.android.apg.provider;
+
+public class SecretKeys extends SecretKeys1 {
+ private SecretKeys() {
+ }
+} \ No newline at end of file
diff --git a/src/org/thialfihar/android/apg/provider/SecretKeys1.java b/src/org/thialfihar/android/apg/provider/SecretKeys1.java
new file mode 100644
index 000000000..3ca405f70
--- /dev/null
+++ b/src/org/thialfihar/android/apg/provider/SecretKeys1.java
@@ -0,0 +1,42 @@
+/*
+ * 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.thialfihar.android.apg.provider;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+class SecretKeys1 implements BaseColumns {
+ public static final String TABLE_NAME = "secret_keys";
+
+ public static final String _ID_type = "INTEGER PRIMARY KEY";
+ public static final String KEY_ID = "c_key_id";
+ public static final String KEY_ID_type = "INT64";
+ public static final String KEY_DATA = "c_key_data";
+ public static final String KEY_DATA_type = "BLOB";
+ public static final String WHO_ID = "c_who_id";
+ public static final String WHO_ID_type = "INTEGER";
+
+ public static final Uri CONTENT_URI =
+ Uri.parse("content://" + DataProvider.AUTHORITY + "/secret_keys");
+ public static final Uri CONTENT_URI_BY_KEY_ID =
+ Uri.parse("content://" + DataProvider.AUTHORITY + "/secret_keys/key_id");
+ public static final String CONTENT_TYPE =
+ "vnd.android.cursor.dir/vnd.thialfihar.apg.secret_key";
+ public static final String CONTENT_ITEM_TYPE =
+ "vnd.android.cursor.item/vnd.thialfihar.apg.secret_key";
+ public static final String DEFAULT_SORT_ORDER = _ID + " DESC";
+} \ No newline at end of file
diff --git a/src/org/thialfihar/android/apg/ui/widget/Editor.java b/src/org/thialfihar/android/apg/ui/widget/Editor.java
new file mode 100644
index 000000000..ab29581a4
--- /dev/null
+++ b/src/org/thialfihar/android/apg/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.thialfihar.android.apg.ui.widget;
+
+public interface Editor {
+ public interface EditorListener {
+ public void onDeleted(Editor editor);
+ }
+
+ public void setEditorListener(EditorListener listener);
+}
diff --git a/src/org/thialfihar/android/apg/ui/widget/KeyEditor.java b/src/org/thialfihar/android/apg/ui/widget/KeyEditor.java
new file mode 100644
index 000000000..263f34675
--- /dev/null
+++ b/src/org/thialfihar/android/apg/ui/widget/KeyEditor.java
@@ -0,0 +1,248 @@
+/*
+ * 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.thialfihar.android.apg.ui.widget;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Vector;
+
+import org.bouncycastle2.openpgp.PGPSecretKey;
+import org.thialfihar.android.apg.Apg;
+import org.thialfihar.android.apg.R;
+import org.thialfihar.android.apg.utils.Choice;
+
+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.ViewGroup;
+import android.view.View.OnClickListener;
+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;
+
+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 static class AlgorithmChoice extends Choice {
+ public static final int DSA = 1;
+ public static final int ELGAMAL = 2;
+ public static final int RSA = 3;
+
+ public AlgorithmChoice(int id, String name) {
+ super(id, name);
+ }
+ }
+
+ public static class UsageChoice extends Choice {
+ public static final int SIGN_ONLY = 1;
+ public static final int ENCRYPT_ONLY = 2;
+ public static final int SIGN_AND_ENCRYPT = 3;
+
+ public UsageChoice(int id, String name) {
+ super(id, name);
+ }
+ }
+
+ 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.key_id);
+ mCreationDate = (TextView) findViewById(R.id.creation);
+ mExpiryDateButton = (Button) findViewById(R.id.expiry);
+ mUsage = (Spinner) findViewById(R.id.usage);
+ KeyEditor.UsageChoice choices[] = {
+ new KeyEditor.UsageChoice(KeyEditor.UsageChoice.SIGN_ONLY,
+ getResources().getString(R.string.sign_only)),
+ new KeyEditor.UsageChoice(KeyEditor.UsageChoice.ENCRYPT_ONLY,
+ getResources().getString(R.string.encrypt_only)),
+ new KeyEditor.UsageChoice(KeyEditor.UsageChoice.SIGN_AND_ENCRYPT,
+ getResources().getString(R.string.sign_and_encrypt)),
+ };
+ ArrayAdapter<KeyEditor.UsageChoice> adapter =
+ new ArrayAdapter<KeyEditor.UsageChoice>(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.edit_delete);
+ mDeleteButton.setOnClickListener(this);
+
+ setExpiryDate(null);
+
+ mExpiryDateButton.setOnClickListener(new OnClickListener() {
+ @Override
+ 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, "None",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ setExpiryDate(null);
+ }
+ });
+ dialog.show();
+ }
+ });
+
+ super.onFinishInflate();
+ }
+
+ public void setValue(PGPSecretKey key, boolean isMasterKey) {
+ mKey = key;
+
+ mIsMasterKey = isMasterKey;
+ if (mIsMasterKey) {
+ mDeleteButton.setVisibility(View.INVISIBLE);
+ }
+
+ mAlgorithm.setText(Apg.getAlgorithmInfo(key));
+ String keyId1Str = Long.toHexString((key.getKeyID() >> 32) & 0xffffffffL);
+ while (keyId1Str.length() < 8) {
+ keyId1Str = "0" + keyId1Str;
+ }
+ String keyId2Str = Long.toHexString(key.getKeyID() & 0xffffffffL);
+ while (keyId2Str.length() < 8) {
+ keyId2Str = "0" + keyId2Str;
+ }
+ mKeyId.setText(keyId1Str + " " + keyId2Str);
+
+ Vector<KeyEditor.UsageChoice> choices = new Vector<KeyEditor.UsageChoice>();
+ choices.add(new KeyEditor.UsageChoice(KeyEditor.UsageChoice.SIGN_ONLY,
+ getResources().getString(R.string.sign_only)));
+ if (!mIsMasterKey) {
+ choices.add(new KeyEditor.UsageChoice(KeyEditor.UsageChoice.ENCRYPT_ONLY,
+ getResources().getString(R.string.encrypt_only)));
+ }
+ choices.add(new KeyEditor.UsageChoice(KeyEditor.UsageChoice.SIGN_AND_ENCRYPT,
+ getResources().getString(R.string.sign_and_encrypt)));
+
+ ArrayAdapter<KeyEditor.UsageChoice> adapter =
+ new ArrayAdapter<KeyEditor.UsageChoice>(getContext(),
+ android.R.layout.simple_spinner_item,
+ choices);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mUsage.setAdapter(adapter);
+
+ if (Apg.isEncryptionKey(key)) {
+ if (Apg.isSigningKey(key)) {
+ mUsage.setSelection(2);
+ } else {
+ mUsage.setSelection(1);
+ }
+ } else {
+ mUsage.setSelection(0);
+ }
+
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.setTime(Apg.getCreationDate(key));
+ mCreationDate.setText(DateFormat.getDateInstance().format(cal.getTime()));
+ cal = new GregorianCalendar();
+ Date date = Apg.getExpiryDate(key);
+ if (date == null) {
+ setExpiryDate(null);
+ } else {
+ cal.setTime(Apg.getExpiryDate(key));
+ setExpiryDate(cal);
+ }
+ }
+
+ public PGPSecretKey getValue() {
+ return mKey;
+ }
+
+ @Override
+ public void onClick(View v) {
+ final ViewGroup parent = (ViewGroup)getParent();
+ if (v == mDeleteButton) {
+ parent.removeView(this);
+ if (mEditorListener != null) {
+ mEditorListener.onDeleted(this);
+ }
+ }
+ }
+
+ @Override
+ 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 UsageChoice getUsage() {
+ return (UsageChoice) mUsage.getSelectedItem();
+ }
+}
diff --git a/src/org/thialfihar/android/apg/ui/widget/SectionView.java b/src/org/thialfihar/android/apg/ui/widget/SectionView.java
new file mode 100644
index 000000000..b257da409
--- /dev/null
+++ b/src/org/thialfihar/android/apg/ui/widget/SectionView.java
@@ -0,0 +1,323 @@
+/*
+ * 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.thialfihar.android.apg.ui.widget;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidParameterException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.util.Vector;
+
+import org.bouncycastle2.openpgp.PGPException;
+import org.bouncycastle2.openpgp.PGPSecretKey;
+import org.thialfihar.android.apg.Apg;
+import org.thialfihar.android.apg.R;
+import org.thialfihar.android.apg.ui.widget.Editor.EditorListener;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class SectionView extends LinearLayout implements OnClickListener, EditorListener, Runnable {
+ public static final int TYPE_USER_ID = 1;
+ public static final int TYPE_KEY = 2;
+
+ private LayoutInflater mInflater;
+ private View mAdd;
+ private ViewGroup mEditors;
+ private TextView mTitle;
+ private int mType = 0;
+
+ private KeyEditor.AlgorithmChoice mNewKeyAlgorithmChoice;
+ private int mNewKeySize;
+
+ volatile private PGPSecretKey mNewKey;
+ private ProgressDialog mProgressDialog;
+ private Thread mRunningThread = null;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ Bundle data = msg.getData();
+ if (data != null) {
+ boolean closeProgressDialog = data.getBoolean("closeProgressDialog");
+ if (closeProgressDialog) {
+ if (mProgressDialog != null) {
+ mProgressDialog.dismiss();
+ mProgressDialog = null;
+ }
+ }
+
+ String error = data.getString("error");
+ if (error != null) {
+ Toast.makeText(getContext(),
+ "Error: " + error,
+ Toast.LENGTH_SHORT).show();
+ }
+
+ boolean gotNewKey = data.getBoolean("gotNewKey");
+ if (gotNewKey) {
+ KeyEditor view =
+ (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item,
+ mEditors, false);
+ view.setEditorListener(SectionView.this);
+ boolean isMasterKey = (mEditors.getChildCount() == 0);
+ view.setValue(mNewKey, isMasterKey);
+ mEditors.addView(view);
+ SectionView.this.updateEditorsVisible();
+ }
+ }
+ }
+ };
+
+ public SectionView(Context context) {
+ super(context);
+ }
+
+ public SectionView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ViewGroup getEditors() {
+ return mEditors;
+ }
+
+ public void setType(int type) {
+ mType = type;
+ switch (type) {
+ case TYPE_USER_ID: {
+ mTitle.setText(R.string.section_userIds);
+ break;
+ }
+
+ case 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 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 TYPE_KEY: {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
+
+ View view = mInflater.inflate(R.layout.create_key, null);
+ dialog.setView(view);
+ dialog.setTitle("Create Key");
+
+ final Spinner algorithm = (Spinner) view.findViewById(R.id.algorithm);
+ KeyEditor.AlgorithmChoice choices[] = {
+ new KeyEditor.AlgorithmChoice(KeyEditor.AlgorithmChoice.DSA,
+ getResources().getString(R.string.dsa)),
+ /*new KeyEditor.AlgorithmChoice(KeyEditor.AlgorithmChoice.ELGAMAL,
+ getResources().getString(R.string.elgamal)),*/
+ new KeyEditor.AlgorithmChoice(KeyEditor.AlgorithmChoice.RSA,
+ getResources().getString(R.string.rsa)),
+ };
+ ArrayAdapter<KeyEditor.AlgorithmChoice> adapter =
+ new ArrayAdapter<KeyEditor.AlgorithmChoice>(
+ 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.length; ++i) {
+ if (choices[i].getId() == KeyEditor.AlgorithmChoice.RSA) {
+ algorithm.setSelection(i);
+ break;
+ }
+ }
+
+ final EditText keySize = (EditText) view.findViewById(R.id.size);
+
+ dialog.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface di, int id) {
+ di.dismiss();
+ try {
+ mNewKeySize = Integer.parseInt("" + keySize.getText());
+ } catch (NumberFormatException e) {
+ mNewKeySize = 0;
+ }
+
+ mNewKeyAlgorithmChoice =
+ (KeyEditor.AlgorithmChoice) algorithm.getSelectedItem();
+ createKey();
+ }
+ });
+
+ dialog.setCancelable(true);
+ dialog.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ 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 != 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) {
+ if (mType != TYPE_KEY) {
+ return;
+ }
+
+ mEditors.removeAllViews();
+ for (PGPSecretKey key : list) {
+ KeyEditor view =
+ (KeyEditor) mInflater.inflate(R.layout.edit_key_key_item, mEditors, false);
+ view.setEditorListener(this);
+ boolean isMasterKey = (mEditors.getChildCount() == 0);
+ view.setValue(key, isMasterKey);
+ mEditors.addView(view);
+ }
+
+ this.updateEditorsVisible();
+ }
+
+ private void createKey() {
+ mProgressDialog = new ProgressDialog(getContext());
+ mProgressDialog.setMessage("Generating key, this can take a while...");
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ mProgressDialog.show();
+ mRunningThread = new Thread(this);
+ mRunningThread.start();
+ }
+
+ public void run() {
+ String error = null;
+ try {
+ mNewKey = Apg.createKey(mNewKeyAlgorithmChoice, mNewKeySize, Apg.getPassPhrase());
+ } catch (NoSuchProviderException e) {
+ error = e.getMessage();
+ } catch (NoSuchAlgorithmException e) {
+ error = e.getMessage();
+ } catch (PGPException e) {
+ error = e.getMessage();
+ } catch (InvalidParameterException e) {
+ error = e.getMessage();
+ } catch (InvalidAlgorithmParameterException e) {
+ error = e.getMessage();
+ } catch (Apg.GeneralException e) {
+ error = e.getMessage();
+ }
+
+ Message message = new Message();
+ Bundle data = new Bundle();
+ data.putBoolean("closeProgressDialog", true);
+ if (error != null) {
+ data.putString("error", error);
+ } else {
+ data.putBoolean("gotNewKey", true);
+ }
+ message.setData(data);
+ mHandler.sendMessage(message);
+ }
+}
diff --git a/src/org/thialfihar/android/apg/ui/widget/UserIdEditor.java b/src/org/thialfihar/android/apg/ui/widget/UserIdEditor.java
new file mode 100644
index 000000000..a7e762b4f
--- /dev/null
+++ b/src/org/thialfihar/android/apg/ui/widget/UserIdEditor.java
@@ -0,0 +1,193 @@
+/*
+ * 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.thialfihar.android.apg.ui.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.thialfihar.android.apg.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+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;
+
+ private static final Pattern EMAIL_PATTERN =
+ Pattern.compile("^([a-zA-Z0-9_.-])+@([a-zA-Z0-9_.-])+[.]([a-zA-Z])+([a-zA-Z])+",
+ 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.edit_delete);
+ mDeleteButton.setOnClickListener(this);
+ mIsMainUserId = (RadioButton) findViewById(R.id.is_main_user_id);
+ 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("invalid email '" + 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;
+ }
+
+ @Override
+ 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();
+ }
+
+ @Override
+ public void setEditorListener(EditorListener listener) {
+ mEditorListener = listener;
+ }
+}
diff --git a/src/org/thialfihar/android/apg/utils/Choice.java b/src/org/thialfihar/android/apg/utils/Choice.java
new file mode 100644
index 000000000..d094c1e1e
--- /dev/null
+++ b/src/org/thialfihar/android/apg/utils/Choice.java
@@ -0,0 +1,44 @@
+/*
+ * 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.thialfihar.android.apg.utils;
+
+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;
+ }
+
+ public String toString() {
+ return mName;
+ }
+}
diff --git a/src/org/thialfihar/android/apg/utils/IterableIterator.java b/src/org/thialfihar/android/apg/utils/IterableIterator.java
new file mode 100644
index 000000000..ff02c4194
--- /dev/null
+++ b/src/org/thialfihar/android/apg/utils/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.thialfihar.android.apg.utils;
+
+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;
+ }
+}