initial commit of v0.8.0

This commit is contained in:
Thialfihar 2010-04-06 19:54:51 +00:00
parent af9342a2cc
commit 42f1720bb3
98 changed files with 8288 additions and 0 deletions

89
AndroidManifest.xml Normal file
View File

@ -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>

13
default.properties Normal file
View File

@ -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

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
res/drawable-hdpi/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
res/drawable-hdpi/key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
res/drawable-ldpi/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
res/drawable-ldpi/key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
res/drawable-mdpi/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
res/drawable-mdpi/key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -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>

52
res/layout/create_key.xml Normal file
View File

@ -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>

View File

@ -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>

58
res/layout/edit_key.xml Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

61
res/layout/main.xml Normal file
View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

68
res/values/strings.xml Normal file
View File

@ -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>

27
res/values/styles.xml Normal file
View File

@ -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>

File diff suppressed because it is too large Load Diff

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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() {
}
}

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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() {
}
}

View File

@ -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";
}

View File

@ -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() {
}
}

View File

@ -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";
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}