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

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